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

37,905 lines 1.2 MB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 var desktopMode = function(exports) {
2 "use strict";
3 var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
4 function installMyWordpressEarlyStub() {
5 const w = window;
6 w.wp = w.wp ?? {};
7 const wp = w.wp;
8 if (!wp.desktop) {
9 wp.desktop = {};
10 }
11 const desktop = wp.desktop;
12 if (desktop.myWordpress) {
13 return;
14 }
15 const queue = [];
16 const stub = {
17 registerEntityKind: (kind, renderer) => {
18 const slot = { unregister: null };
19 const entry = { kind, renderer, slot };
20 queue.push(entry);
21 return () => {
22 if (slot.unregister) {
23 slot.unregister();
24 slot.unregister = null;
25 return;
26 }
27 const i = queue.indexOf(entry);
28 if (i !== -1) {
29 queue.splice(i, 1);
30 }
31 };
32 },
33 __pendingKinds: queue
34 };
35 desktop.myWordpress = stub;
36 }
37 installMyWordpressEarlyStub();
38 function getWpHooks$1() {
39 const hooks = window.wp?.hooks;
40 if (!hooks) {
41 throw new Error(
42 "[desktop-mode] `window.wp.hooks` is not available. The plugin declares `wp-hooks` as a script dependency; if you are seeing this error, verify the enqueue order."
43 );
44 }
45 return hooks;
46 }
47 function addFilter(hookName2, namespace, callback, priority) {
48 getWpHooks$1().addFilter(
49 hookName2,
50 namespace,
51 callback,
52 priority
53 );
54 }
55 function addAction(hookName2, namespace, callback, priority) {
56 getWpHooks$1().addAction(
57 hookName2,
58 namespace,
59 callback,
60 priority
61 );
62 }
63 function removeAction(hookName2, namespace) {
64 return getWpHooks$1().removeAction(hookName2, namespace);
65 }
66 function applyFilters(hookName2, value, ...args) {
67 return getWpHooks$1().applyFilters(hookName2, value, ...args);
68 }
69 function doAction(hookName2, ...args) {
70 getWpHooks$1().doAction(hookName2, ...args);
71 }
72 function didAction(hookName2) {
73 return getWpHooks$1().didAction(hookName2);
74 }
75 function rawHooks() {
76 return getWpHooks$1();
77 }
78 const HOOKS = {
79 /** Action, fires once after shell boot; plugins register here. */
80 INIT: "desktop-mode.init",
81 /** Filter, receives the wallpaper registry array. */
82 WALLPAPERS: "desktop-mode.wallpapers",
83 /**
84 * Filter, receives the games registry array (`GameRegistryEntry[]`)
85 * on every read. Mirrors the PHP-side `desktop_mode_games` filter.
86 *
87 * @since 0.9.6
88 */
89 GAMES: "desktop-mode.games",
90 /** Filter, receives the unfocused-window effect registry array. */
91 UNFOCUS_EFFECTS: "desktop-mode.unfocus-effects",
92 /** Action before a canvas wallpaper mounts. */
93 WALLPAPER_MOUNTING: "desktop-mode.wallpaper.mounting",
94 /** Action after a canvas wallpaper mounts successfully. */
95 WALLPAPER_MOUNTED: "desktop-mode.wallpaper.mounted",
96 /** Action before a canvas wallpaper tears down. */
97 WALLPAPER_UNMOUNTING: "desktop-mode.wallpaper.unmounting",
98 /** Action when a canvas wallpaper's mount throws / rejects. */
99 WALLPAPER_MOUNT_FAILED: "desktop-mode.wallpaper.mount-failed",
100 /** Action mirroring document.visibilitychange for active canvas wallpapers. */
101 WALLPAPER_VISIBILITY: "desktop-mode.wallpaper.visibility",
102 /**
103 * Action, fires when the wallpaper enters or leaves the suspended
104 * state (`wp.desktop.wallpaper.suspend()/resume()` — e.g. while a
105 * game is running). Payload: `{ id, suspended, reasons }` — the
106 * active canvas wallpaper id (or null), whether the layer is now
107 * suspended, and the currently-held reason strings. Suspension also
108 * re-emits `WALLPAPER_VISIBILITY` with the effective state, so
109 * wallpapers that only wire the visibility action pause for free.
110 *
111 * @since 0.9.6
112 */
113 WALLPAPER_SUSPEND: "desktop-mode.wallpaper.suspend",
114 /**
115 * Filter, receives a wallpaper's preview params (seeded from the
116 * def's `previewParams`) before its `renderPreview` runs in the OS
117 * Settings picker. Args: `( params, wallpaperId )`.
118 */
119 WALLPAPER_PREVIEW_PARAMS: "desktop-mode.wallpaper.preview-params",
120 /**
121 * Action, fires after a wallpaper's persisted settings change (the
122 * user edited them through the wallpaper's config dialog in OS
123 * Settings). Payload: `{ id, settings }` — the wallpaper id and the
124 * full post-merge settings object. A mounted wallpaper subscribes to
125 * live-apply changes without a remount.
126 *
127 * @since 0.9.5
128 */
129 WALLPAPER_SETTINGS_CHANGED: "desktop-mode.wallpaper.settings-changed",
130 // ------------------------------------------------------------------
131 // Observability — iframe errors, iframe network, shell-side errors,
132 // monitor entry aggregation. Designed for dashboard / debug widget
133 // plugins that want genuine admin observability (Gutenberg save
134 // failures, admin-ajax 500s, plugin exceptions) rather than just the
135 // shell's own console-error surface.
136 // ------------------------------------------------------------------
137 /**
138 * Action, fires once per iframe when the chromeless bridge
139 * script has finished wiring its message listeners. Payload:
140 * `{ windowId: string }`. Subscribers get a reliable "safe to
141 * talk to this iframe" signal — the browser's native `load`
142 * event fires before our bridge attaches, so messages sent on
143 * `load` can be dropped on the floor. Use this instead when
144 * timing matters (first-focus dispatch, auto-fill handshakes).
145 *
146 * @since 0.5.0
147 */
148 IFRAME_READY: "desktop-mode.iframe.ready",
149 /**
150 * Action, fires when a chromeless iframe's `error` or
151 * `unhandledrejection` handler catches an exception. Payload: `{
152 * windowId: string, kind: 'error' | 'unhandledrejection', message:
153 * string, filename: string | null, lineno: number | null, colno:
154 * number | null, stack: string | null }`. Origin-filtered at the
155 * parent shell; cross-origin iframe errors never reach here.
156 */
157 IFRAME_ERROR: "desktop-mode.iframe.error",
158 /**
159 * Action, fires when a `fetch` or `XMLHttpRequest` inside a
160 * chromeless iframe completes (success OR failure). Payload: `{
161 * windowId: string, method: string, url: string, status: number,
162 * duration: number, failed: boolean }`. Subscribers get a faithful
163 * view of admin-ajax + REST calls that previously never left the
164 * iframe boundary. `status === 0` indicates a network failure with
165 * no response received.
166 */
167 IFRAME_NETWORK_COMPLETED: "desktop-mode.iframe.network-completed",
168 /**
169 * Action, fires when one of the shell's own try/catch barriers
170 * catches an exception. Payload: `{ scope:
171 * 'widget-mount' | 'widget-teardown' | 'window-open' | 'wallpaper-mount' |
172 * 'wallpaper-teardown' | 'session-save' | 'menu-refresh' | string,
173 * id?: string, error: unknown }`. Paired with the existing
174 * `console.error` calls — a monitor widget can surface these as
175 * first-class entries.
176 */
177 SHELL_ERROR: "desktop-mode.shell.error",
178 /**
179 * Action, fires once per `wp.desktop.broadcast()` call with the
180 * fully-resolved `{ topic, payload }` detail. Lets plugins log,
181 * mirror, or augment broadcast traffic without subscribing for
182 * every individual topic.
183 */
184 BROADCAST: "desktop-mode.broadcast",
185 /**
186 * Filter, applies to a `MonitorEntry` before a monitor widget
187 * renders it. Plugins can mutate the entry (rewrite the message,
188 * add `extra` fields) or return `null` to suppress it. Used by
189 * monitor widgets to converge every plugin on the same shape —
190 * see `MonitorEntry` in `src/types.ts`.
191 */
192 MONITOR_ENTRY: "desktop-mode.monitor.entry",
193 /**
194 * Filter, applies to the list of "solid" surfaces wallpapers
195 * should consider for collision / accumulation effects (snow
196 * piling, leaves settling, rain splash). Seeded by the shell
197 * with: every visible (non-minimized) window's top edge; the
198 * desktop-area floor; the dock's outward-facing edge; and every
199 * mounted widget card's top edge.
200 *
201 * Plugins that own their own DOM (e.g. floating pickers,
202 * custom overlays) can push additional surfaces so snow
203 * accumulates on them too.
204 *
205 * Each entry is a `WallpaperSurface` — see
206 * `src/wallpapers/surfaces.ts` for the shape. Rects are in
207 * viewport coordinates (clientX / clientY), matching what a
208 * canvas mounted inside `#desktop-mode-wallpaper` reads.
209 */
210 WALLPAPER_SURFACES: "desktop-mode.wallpaper.surfaces",
211 // ------------------------------------------------------------------
212 // Window lifecycle actions. All payloads share a `windowId: string`
213 // field; additional fields are documented per-hook in the JS
214 // reference. These mirror the existing `desktop-mode-window-*`
215 // CustomEvents but ship under the hook bus so plugins can use one
216 // idiomatic API for everything the shell emits.
217 // ------------------------------------------------------------------
218 /**
219 * Filter, last call before a window's resolved geometry (x, y,
220 * width, height, initialState) is baked into the `WindowConfig`
221 * passed to the `Window` constructor. Lets a plugin override
222 * default placement for windows it owns, snap restored bounds to
223 * a different region, or force a particular initial state.
224 *
225 * Signature:
226 *
227 * ( geometry: ResolvedWindowGeometry, ctx: WindowGeometryContext )
228 * => ResolvedWindowGeometry
229 *
230 * Where `ResolvedWindowGeometry = { x, y, width, height, state? }`
231 * and `ctx = { windowId, baseId, hasSavedGeometry, callerPinned,
232 * desktopRect }`.
233 *
234 * - `hasSavedGeometry` is `true` when the user previously
235 * dragged or resized this window and the resolved geometry
236 * includes those restored values. Plugins that want to
237 * "leave the user's saved layout alone" should bail when
238 * this is true.
239 * - `callerPinned` is `true` when the caller of `manager.open()`
240 * passed at least one of `{ x, y, width, height, initialState }`
241 * explicitly. For NATIVE windows this is usually true (the
242 * framework's native-window opener passes the registry's
243 * declared dimensions); for admin-page iframe windows opened
244 * from the dock this is usually false. The filter is free to
245 * override registry defaults — `callerPinned: true` does NOT
246 * mean "leave it alone."
247 *
248 * The shell re-clamps `width`/`height` to the registered
249 * `minWidth`/`minHeight` after the filter returns — a buggy
250 * filter cannot ship a sub-minimum window. `x` and `y` are
251 * NOT re-clamped to the desktop rect after the filter (plugins
252 * sometimes want to place windows partially off-screen for
253 * deliberate stylistic reasons); the filter is responsible for
254 * its own viewport math when it cares.
255 *
256 * Companion of `desktop_mode_register_window` server-side
257 * defaults — runs every time a window opens, not just at
258 * registration.
259 *
260 * @since 0.8.6
261 */
262 WINDOW_GEOMETRY: "desktop-mode.window.geometry",
263 /** Action, fires when a window is added to the stack. */
264 WINDOW_OPENED: "desktop-mode.window.opened",
265 /**
266 * Action, fires when a window's body enters the loading state — at
267 * construction (every window starts loading) and whenever a plugin
268 * calls {@link NativeRenderContext.window.markLoading} or
269 * `Window.markContentLoading()` mid-life. Payload: `{ windowId }`.
270 *
271 * The shell shows a `<wpd-spinner>` overlay while the window is in
272 * the loading state and fades content in on the loaded transition.
273 * Subscribe to this hook (or to {@link WINDOW_CONTENT_LOADED}) when
274 * you need to react to either edge — analytics, instrumentation,
275 * decorating the spinner with a per-window message.
276 *
277 * Edge-triggered: idempotent calls don't re-fire. The matching
278 * `desktop-mode-window-content-loading` CustomEvent dispatches on
279 * `document` with the same payload.
280 *
281 * @since 0.6.0
282 */
283 WINDOW_CONTENT_LOADING: "desktop-mode.window.content-loading",
284 /**
285 * Action, fires when a window's body content becomes ready — for
286 * iframe windows the moment the chromeless bridge announces
287 * `desktop-mode-ready`, for native windows after the user's
288 * `render( body )` callback (or its returned promise) resolves, and
289 * whenever a plugin calls {@link NativeRenderContext.window.markReady}
290 * or `Window.markContentLoaded()` mid-life. Payload: `{ windowId }`.
291 *
292 * The unified "window content is ready" signal across both render
293 * strategies — use this instead of branching on iframe vs. native.
294 * Iframe-only consumers can still subscribe to {@link IFRAME_READY},
295 * which fires alongside this hook for iframe windows. The shell
296 * removes the loading overlay and fades the content in on this
297 * transition.
298 *
299 * Edge-triggered: only fires on a loading → ready transition.
300 * The matching `desktop-mode-window-content-loaded` CustomEvent
301 * dispatches on `document` with the same payload.
302 *
303 * @since 0.6.0
304 */
305 WINDOW_CONTENT_LOADED: "desktop-mode.window.content-loaded",
306 /**
307 * Filter, applied to the loading-overlay HTMLElement just after
308 * the shell paints its default `<wpd-spinner>` and after any
309 * per-window inline customization (`config.loading.render`)
310 * runs. Receives the overlay element; context: `{ windowId,
311 * config }`. Plugins may mutate the element (e.g.
312 * `host.replaceChildren( myBrandedLoader )` to swap out the
313 * default entirely, or `host.querySelector('wpd-spinner')!.
314 * setAttribute('preset', 'comet')` to retune the spinner) or
315 * return a different element to replace the overlay wholesale.
316 *
317 * Use cases: a brand-skin plugin that overrides every window's
318 * spinner with its own logo; a status-bar plugin that adds
319 * "Loading… 47% — fetching posts" text; an A/B-test framework
320 * that swaps the loader during an experiment.
321 *
322 * Resolution order for the loading overlay:
323 * 1. Default content (`<wpd-spinner>`) is painted.
324 * 2. Per-window `config.loading.render( host, ctx )` runs.
325 * 3. This filter runs.
326 * 4. The result is appended to the window body.
327 *
328 * @since 0.6.0
329 */
330 WINDOW_LOADING_OVERLAY: "desktop-mode.window.loading-overlay",
331 /**
332 * Action, fires when `manager.open(...)` is called for a baseId
333 * whose window already exists on the active desktop. This is the
334 * unambiguous "user requested to open this window again" signal
335 * — distinct from focus changes (which double-fire on alt-tab and
336 * skip when already focused) and from `WINDOW_OPENED` (which only
337 * fires on first creation). Payload:
338 * `{ windowId: string, baseId: string, wasMinimized: boolean }`.
339 *
340 * Plugins that hold per-window state (e.g. the code-editor's
341 * active file) should listen here to re-orient the existing
342 * window's content to whatever the caller wants to show — the
343 * open-window call is synchronous, so any state the caller sets
344 * BEFORE invoking `openWindow` is already in place when this
345 * fires.
346 */
347 WINDOW_REOPENED: "desktop-mode.window.reopened",
348 /**
349 * Action, fires BEFORE the window's element is detached from the
350 * DOM but AFTER the manager has already removed it from the stack.
351 * Payload: `{ windowId: string, element: HTMLElement }`.
352 *
353 * Use this for cleanup that needs a reference to the live
354 * element (removing anchored snow, wallpaper particles pinned to
355 * window tops, measurement caches keyed by element). `WINDOW_CLOSED`
356 * fires immediately after and only carries the id, which means
357 * subscribers would otherwise have to re-query the DOM — by then
358 * the element is gone, so they can't match at all.
359 */
360 WINDOW_CLOSING: "desktop-mode.window.closing",
361 /** Action, fires when a window is removed from the stack. */
362 WINDOW_CLOSED: "desktop-mode.window.closed",
363 /** Action, fires when focus changes to a different window. */
364 WINDOW_FOCUSED: "desktop-mode.window.focused",
365 /**
366 * Action, fires for the window that LOST focus when another
367 * window takes over. Symmetric counterpart to
368 * `WINDOW_FOCUSED`. Payload: `{ windowId: string, focusedTo:
369 * string | null }` — `focusedTo` identifies the new top of
370 * the stack so blur subscribers can ignore alt-tabs to a
371 * sibling they own.
372 *
373 * No-op when there's no previously-focused window (initial
374 * boot, all-windows-closed). Manager fires this BEFORE
375 * `WINDOW_FOCUSED` so subscribers see "blur old, focus new"
376 * in deterministic order.
377 *
378 * @since 0.5.5
379 */
380 WINDOW_BLURRED: "desktop-mode.window.blurred",
381 /**
382 * Action, fires when a window is minimized. Payload:
383 * `{ windowId: string, element: HTMLElement }`.
384 *
385 * The element ride-along matches {@link WINDOW_CLOSING}'s shape so
386 * wallpaper plugins anchored to window tops (snow, leaves, rain
387 * splash) can match stuck particles by element identity and run
388 * their teardown — minimized windows render at `opacity: 0` so
389 * `offsetParent === null` checks miss them.
390 */
391 WINDOW_MINIMIZED: "desktop-mode.window.minimized",
392 /**
393 * Action, fires when a window is restored from minimized. Payload:
394 * `{ windowId: string, element: HTMLElement }`.
395 */
396 WINDOW_RESTORED: "desktop-mode.window.restored",
397 /**
398 * Action, fires when a window is maximized (fills desktop area).
399 * Payload: `{ windowId: string, element: HTMLElement }`.
400 */
401 WINDOW_MAXIMIZED: "desktop-mode.window.maximized",
402 /**
403 * Action, fires when a window exits maximized state. Payload:
404 * `{ windowId: string, element: HTMLElement }`.
405 */
406 WINDOW_UNMAXIMIZED: "desktop-mode.window.unmaximized",
407 /**
408 * Action, fires when a window enters fullscreen / focus mode.
409 * Payload: `{ windowId: string, element: HTMLElement }`.
410 */
411 WINDOW_FULLSCREEN_ENTERED: "desktop-mode.window.fullscreen-entered",
412 /**
413 * Action, fires when a window exits fullscreen / focus mode.
414 * Payload: `{ windowId: string, element: HTMLElement }`.
415 */
416 WINDOW_FULLSCREEN_EXITED: "desktop-mode.window.fullscreen-exited",
417 /**
418 * Filter, decides whether a fullscreen ("focus mode") window
419 * should auto-exit when focus moves to a different window.
420 *
421 * Default is `true` so a newly-focused window is never silently
422 * occluded by a fullscreen one (its `z-index` sits above all
423 * other windows). Plugins whose fullscreen surface is meant to
424 * persist across focus changes — slideshows, video players,
425 * immersive games — can return `false` to keep their window
426 * fullscreen.
427 *
428 * Signature:
429 *
430 * ( shouldExit: boolean, ctx: {
431 * windowId: string, // the fullscreen window
432 * focusedTo: string, // the window gaining focus
433 * } ) => boolean
434 *
435 * @since 0.8.6
436 */
437 WINDOW_AUTO_EXIT_FULLSCREEN: "desktop-mode.window.auto-exit-fullscreen",
438 /**
439 * Filter, decides whether the window under the cursor is raised
440 * (focused) after a short hover dwell during a drag — any drag,
441 * whatever its source: a shell DragManager session, a
442 * cross-iframe bridge drag, an OS file, or an arbitrary native
443 * HTML5 drag.
444 *
445 * Default is `true`: dragging a payload over a background window
446 * and resting there for ~250 ms brings it forward, so the user
447 * can see the drop target they're aiming at (macOS spring-loading
448 * style). Plugins whose windows must never steal z-order during a
449 * drag — pinned reference panels, HUD/palette windows — can
450 * return `false` for their window id.
451 *
452 * Signature:
453 *
454 * ( shouldFocus: boolean, ctx: {
455 * windowId: string, // the hovered window
456 * payloadType: string, // DragManager payload `type`,
457 * // bridge payload `kind`,
458 * // 'os-file', or 'external'
459 * } ) => boolean
460 *
461 * @since 0.9.4
462 */
463 WINDOW_FOCUS_ON_DRAG_HOVER: "desktop-mode.window.focus-on-drag-hover",
464 /**
465 * Action, fires at most once per animation frame during an
466 * active drag or resize with the live geometry. Payload: `{
467 * windowId: string, x: number, y: number, width: number,
468 * height: number, state: WindowState, phase: 'drag' | 'resize' }`.
469 *
470 * Intended for per-frame collision-aware wallpapers (snow piling
471 * on window tops, rain splash on edges) that would otherwise
472 * poll `getBoundingClientRect` every rAF. Coalesced via
473 * `requestAnimationFrame` so a pointermove storm collapses to
474 * one fire per paint — matches the cadence a wallpaper's own
475 * ticker runs at.
476 *
477 * NOT fired at drag/resize end — `WINDOW_DRAG_END` /
478 * `WINDOW_RESIZE_END` handle the settled geometry. Subscribers
479 * that only want the final position should listen to those
480 * instead.
481 */
482 WINDOW_BOUNDS_CHANGED: "desktop-mode.window.bounds-changed",
483 /** Action, fires at drag-end with the final `{ x, y }` position. */
484 WINDOW_MOVED: "desktop-mode.window.moved",
485 /** Action, fires at resize-end with the final `{ width, height }`. */
486 WINDOW_RESIZED: "desktop-mode.window.resized",
487 /** Action, fires when title-bar drag begins. */
488 WINDOW_DRAG_START: "desktop-mode.window.drag-start",
489 /** Action, fires when title-bar drag ends. Payload mirrors WINDOW_MOVED. */
490 WINDOW_DRAG_END: "desktop-mode.window.drag-end",
491 /** Action, fires when the resize handle is first pressed. */
492 WINDOW_RESIZE_START: "desktop-mode.window.resize-start",
493 /** Action, fires when resize completes. Payload mirrors WINDOW_RESIZED. */
494 WINDOW_RESIZE_END: "desktop-mode.window.resize-end",
495 /** Action, fires when the user "detaches" a window to a classic tab. */
496 WINDOW_DETACHED: "desktop-mode.window.detached",
497 /**
498 * Action, fires when the user clicks the title-bar reload button
499 * on an iframe-backed window. Payload: `{ windowId: string, url:
500 * string }` where `url` is the URL being reloaded (the active
501 * primary or external sub-tab). Subscribers can use this to
502 * invalidate their own cache, force a save before navigation,
503 * track usage as a UX signal, or sync state across companion
504 * surfaces. Native windows do not fire this — they own their
505 * DOM directly and the reload button doesn't apply.
506 */
507 WINDOW_RELOADED: "desktop-mode.window.reloaded",
508 /** Action, fires when iframe title updates change the window title. */
509 WINDOW_TITLE_CHANGED: "desktop-mode.window.title-changed",
510 /**
511 * Action, fires when a window's `setHighlight()` mode changes.
512 * Payload: `{ windowId: string, mode: 'preview' | 'persistent' | null,
513 * color?: string }`. Lets onboarding / guidance / drag-bridge
514 * plugins react when another module flagged one of their
515 * windows as the focus of a multi-step interaction without
516 * having to observe DOM mutations.
517 *
518 * @since 0.6.0
519 */
520 WINDOW_HIGHLIGHT_CHANGED: "desktop-mode.window.highlight-changed",
521 /**
522 * Action, fires when a window's body element's dimensions
523 * change — mount, user resize, viewport reflow. Payload: `{
524 * windowId: string, width: number, height: number }`. Body
525 * dimensions exclude the title bar + tab strip, matching what a
526 * canvas or layout engine inside the body would measure.
527 */
528 WINDOW_BODY_RESIZED: "desktop-mode.window.body-resized",
529 // ------------------------------------------------------------------
530 // Native-window lifecycle. These fire ONLY for windows constructed
531 // with `native: true` — iframe windows have no render phase to
532 // intercept. Use them to wrap / instrument / cancel the paint of
533 // plugin-contributed native windows (the Calculator, Jorvy, custom
534 // native launchers).
535 // ------------------------------------------------------------------
536 /**
537 * Filter, applied to the body element a native window will render
538 * into, just BEFORE the user's `render( body )` callback runs.
539 * Payload: the `HTMLElement`; context: `{ windowId, config }`.
540 *
541 * Return the same element (or a wrapper) to intercept. Subscribers
542 * commonly use this to inject a consistent shell (padding,
543 * background, decorative chrome) around every native window
544 * without every plugin re-implementing the pattern.
545 */
546 NATIVE_WINDOW_BEFORE_RENDER: "desktop-mode.native-window.before-render",
547 /**
548 * Action, fires AFTER a native window's `render( body )` callback
549 * returns. Payload: `{ windowId, body, config }`. Observability
550 * hook — analytics / auto-focus / post-render measurement.
551 */
552 NATIVE_WINDOW_AFTER_RENDER: "desktop-mode.native-window.after-render",
553 /**
554 * Filter, applied when a native window is about to start its
555 * close animation. Return `false` to CANCEL the close — the
556 * window stays open. Payload: `true`; context: `{ windowId,
557 * config }`. Any non-`false` return (including `undefined`) lets
558 * the close proceed.
559 *
560 * Intended for "unsaved changes" guards: a calculator with a
561 * pending operation can prompt the user and abort the close
562 * mid-flight. Does NOT apply to iframe windows — their close is
563 * driven by browser navigation patterns the shell doesn't own.
564 */
565 NATIVE_WINDOW_BEFORE_CLOSE: "desktop-mode.native-window.before-close",
566 // ------------------------------------------------------------------
567 // Window-chrome customization framework. Plugins drive per-window
568 // appearance (theme, controls, slots, full chrome render) through
569 // the `wp.desktop.registerWindow*` registries; these hooks expose
570 // every resolution step so plugins can mutate or observe the
571 // chrome pipeline without owning a registration.
572 //
573 // Layers 1-3 (theme, controls, slots) are Stable. Layer 4 (chrome
574 // render) is Experimental — `WINDOW_CHROME_RENDER` may change.
575 // ------------------------------------------------------------------
576 /**
577 * Filter, applied to the resolved CSS-variable map for a window.
578 * Receives `Record< string, string >`; context: `{ windowId,
579 * config }`. Plugins return a mutated map to override or augment
580 * the per-window theme tokens — e.g. tint every Gutenberg
581 * window's title bar to brand colour.
582 *
583 * Stable since 0.6.0.
584 */
585 WINDOW_CHROME_THEME: "desktop-mode.window.chrome.theme",
586 /**
587 * Filter, applied to the resolved control list for a window.
588 * Receives `WindowControlDef[]`; context: `{ windowId, config,
589 * placement: 'left' | 'right' | 'controls' }`. Plugins return a
590 * mutated array to reorder, hide, or inject controls per-window.
591 *
592 * Stable since 0.6.0.
593 */
594 WINDOW_CHROME_CONTROLS: "desktop-mode.window.chrome.controls",
595 /**
596 * Filter, applied per slot when the chrome paints. Receives the
597 * slot host element; context: `{ windowId, slot, config }`.
598 * Plugins can mutate `host` (append decorative children, set
599 * inline styles) without owning a `WindowSlotDef` registration.
600 * The shell never reads the return value — this is an action-
601 * shaped filter so existing `addFilter` plumbing applies.
602 *
603 * Stable since 0.6.0.
604 */
605 WINDOW_CHROME_SLOT: "desktop-mode.window.chrome.slot",
606 /**
607 * Filter, applied to the chrome id selected for a window.
608 * Receives the resolved id (defaults to `'core/standard'`);
609 * context: `{ windowId, config }`. Returning a different id
610 * swaps the chrome registration. **Experimental** — chrome
611 * render contract may change.
612 *
613 * @since 0.6.0
614 */
615 WINDOW_CHROME_RENDER: "desktop-mode.window.chrome.render",
616 /**
617 * Action, fires after a window chrome layer has been mounted /
618 * remounted. Payload: `{ windowId, layer: 'chrome' | 'controls'
619 * | 'slots', chromeId? }` — `chromeId` is present only when
620 * `layer` is `'chrome'`. Subscribers can post-decorate the
621 * chrome (attach observers, anchor pickers).
622 *
623 * @since 0.6.0
624 */
625 WINDOW_CHROME_APPLIED: "desktop-mode.window.chrome.applied",
626 /**
627 * Action, fires after a window's theme tokens are applied to its
628 * outer element. Payload: `{ windowId, themeId, tokens }`. Lets
629 * plugins react to theme changes without diffing CSS variables.
630 *
631 * @since 0.6.0
632 */
633 WINDOW_CHROME_THEME_CHANGED: "desktop-mode.window.chrome.theme-changed",
634 /**
635 * Action, fires when a user clicks a desktop icon (a shortcut
636 * tile registered server-side via `desktop_mode_register_icon()`
637 * and rendered on the wallpaper). Payload: `{ id: string,
638 * target: 'window' | 'url' }`. Fires BEFORE the default open
639 * action — plugins cannot cancel the open from this hook, but
640 * can use it to track click-throughs or augment behaviour (e.g.
641 * play a sound, surface a confirmation toast).
642 *
643 * @since 0.5.0
644 */
645 DESKTOP_ICON_CLICKED: "desktop-mode.desktop-icon.clicked",
646 /**
647 * Action, fires after the wallpaper icon grid is rendered or
648 * re-rendered. Payload:
649 *
650 * {
651 * ids: string[]; // paint order
652 * container: HTMLElement; // <div class="desktop-mode-icons">
653 * tiles: ReadonlyMap<string, HTMLElement>; // id → tile <button>
654 * }
655 *
656 * Plugins that decorate icons with surfaces the framework doesn't
657 * natively expose (drag handles, status dots, cursor adornments)
658 * subscribe here so their decorations survive a live menu refresh
659 * that legitimately rebuilds the grid. The `container` and
660 * `tiles` map mirror the {@link DOCK_AFTER_RENDER}
661 * `tileElements` contract — reach into them directly instead of
662 * re-`querySelector`ing the rendered DOM.
663 *
664 * Notification badges have a first-class API since 0.6.0 —
665 * use `wp.desktop.icons.setBadge( id, count )` (and subscribe
666 * to {@link ICON_BADGE_CHANGED}) instead of decorating from
667 * here. The framework persists badge state across rebuilds, so
668 * a plugin that uses the API doesn't need to re-decorate on
669 * every render.
670 *
671 * Suppressed entirely when the rendered DOM is unchanged from
672 * the previous call (the fingerprint short-circuit upstream
673 * skips both the rebuild and this signal). When the icon list
674 * is empty the hook does not fire at all — the previous
675 * container is removed and no new one is appended.
676 *
677 * @since 0.6.0
678 * @since 0.8.6 — `container` + `tiles` added to the payload
679 * (`ids` retained for back-compat).
680 */
681 DESKTOP_ICONS_RENDERED: "desktop-mode.desktop-icons.rendered",
682 /**
683 * Action, fires whenever the badge count on a desktop icon
684 * changes. Payload: `{ iconId: string, count: number,
685 * previousCount: number }`. Symmetric to {@link DOCK_ITEM_APPENDED}
686 * and the dock/taskbar `wpd-dock-item-badge-changed` CustomEvent
687 * — the icon rail's lifecycle hook for badge transitions.
688 *
689 * Mirrors `desktop-mode/badge-changed` on the activity bus with
690 * `rail: 'icon'`. Subscribe to whichever surface fits — the
691 * activity channel composes across rails for global widgets,
692 * this hook fires only for icon-rail badges with the previous
693 * count carried alongside for delta-aware consumers.
694 *
695 * @since 0.6.0
696 */
697 ICON_BADGE_CHANGED: "desktop-mode.icon.badge-changed",
698 // ------------------------------------------------------------------
699 // Cross-plugin composition.
700 // ------------------------------------------------------------------
701 /**
702 * Action, fires ONCE after every shell-shipped `<wpd-*>` custom
703 * element has registered with `customElements`. Payload: `{
704 * tags: string[] }` — the list of registered tag names. Plugins
705 * that need to defer work until the component registry is
706 * complete (e.g. hydrate user content that uses these tags)
707 * subscribe here instead of polling `customElements.get()`.
708 */
709 COMPONENTS_REGISTERED: "desktop-mode.components.registered",
710 /**
711 * Action, fires after `wp.desktop.registerSystemTile()` inserts
712 * a tile into the unified dock. Payload: `{ id: string }`. Useful
713 * for plugins that want to decorate tiles they didn't register
714 * themselves — analytics, theming, per-tile badges.
715 */
716 DOCK_ITEM_APPENDED: "desktop-mode.dock.item-appended",
717 /**
718 * Action, fires after a system tile is removed from a rail
719 * via `Dock.removeSystemItem()` (typically the server-driven
720 * native-window-sync path on plugin deactivation). Payload:
721 * `{ id: string, placement: 'dock' | 'taskbar' }`. Symmetric
722 * to {@link DOCK_ITEM_APPENDED}; lets analytics / decorators /
723 * cleanup hooks see the full lifecycle without polling the DOM.
724 *
725 * @since 0.6.0
726 */
727 DOCK_ITEM_REMOVED: "desktop-mode.dock.item-removed",
728 // ------------------------------------------------------------------
729 // Dock decoration hooks — render-pipeline filters and actions the
730 // default `Dock` renderer fires while painting tiles. Plugins
731 // compose decoration (animations, classNames, wrappers, tooltips)
732 // without forking the renderer. Custom rail renderers SHOULD fire
733 // the same hooks for ecosystem compatibility — see
734 // `docs/examples/dock-decoration-hooks.md` for the contract.
735 //
736 // Every detail object carries `{ rail, orientation, dockId,
737 // container }` so a single subscriber can disambiguate when two
738 // rails coexist (Classic layout's left side bar + bottom dock).
739 // `dockId` matches the host element's `id` (e.g. `'desktop-mode-dock'`
740 // or `'desktop-mode-side-dock'`) and is the stable
741 // disambiguator — `rail` and `orientation` are convenience
742 // projections of where the renderer is painting.
743 // ------------------------------------------------------------------
744 /**
745 * Action, fires at the start of every dock paint pass — both the
746 * initial mount and every `replaceItems()` that follows on the
747 * live menu-refresh path. Payload `DockRenderContext`. Use this
748 * to invalidate cached per-render decoration state before the
749 * tiles repopulate.
750 *
751 * @since 0.5.2
752 */
753 DOCK_BEFORE_RENDER: "desktop-mode.dock.before-render",
754 /**
755 * Action, fires once every menu and system tile has landed in
756 * the DOM for a paint pass. Payload `DockRenderContext` plus a
757 * frozen `tileElements: ReadonlyMap<string, HTMLElement>` so a
758 * plugin can decorate every tile in one sweep. Symmetric to
759 * {@link DOCK_BEFORE_RENDER}.
760 *
761 * @since 0.5.2
762 */
763 DOCK_AFTER_RENDER: "desktop-mode.dock.after-render",
764 /**
765 * Filter, runs once per tile while the renderer is composing the
766 * className list. Plugins may add, remove, or reorder classes.
767 * Signature: `( classes: string[], detail: DockTileContext ) =>
768 * string[]`. Order is preserved.
769 *
770 * @since 0.5.2
771 */
772 DOCK_TILE_CLASS: "desktop-mode.dock.tile-class",
773 /**
774 * Filter, runs once per tile after the renderer finishes building
775 * the element but before it lands in the DOM. Return the same
776 * element with mutations, or replace with a wrapper — the shell
777 * inserts whatever you return. Signature:
778 * `( el: HTMLElement, detail: DockTileContext ) => HTMLElement`.
779 *
780 * Returning a different node still has to expose a stable
781 * `[data-menu-slug="<id>"]` (or `[data-system-id="<id>"]`)
782 * descendant for active-state / badge updates to find the tile;
783 * wrap, don't replace.
784 *
785 * @since 0.5.2
786 */
787 DOCK_TILE_ELEMENT: "desktop-mode.dock.tile-element",
788 /**
789 * Action, fires once per tile after it has been inserted into
790 * the DOM. Payload `DockTileContext` plus the resolved `el`. Use
791 * for post-insertion decoration where computed layout matters
792 * (measurements, IntersectionObserver bindings, etc.).
793 *
794 * @since 0.5.2
795 */
796 DOCK_TILE_RENDERED: "desktop-mode.dock.tile-rendered",
797 /**
798 * Filter, resolves the tooltip text for a tile. Runs once at
799 * bind time so the dock doesn't re-filter on every pointerenter.
800 * Signature: `( label: string, detail: DockTileContext ) =>
801 * string`. Return an empty string to suppress the tooltip.
802 *
803 * @since 0.5.2
804 */
805 DOCK_TILE_TOOLTIP: "desktop-mode.dock.tile-tooltip",
806 /**
807 * Filter, resolves the body content of a single hover-peek card.
808 * Runs once per card build (i.e., on every show of the peek for
809 * a multi-instance dock tile that has ≥1 open window). Lets a
810 * plugin render a custom thumbnail, status block, or any other
811 * markup inside the card in place of (or alongside) the default
812 * mini-window styling.
813 *
814 * Signature:
815 * ( body: HTMLElement, detail: DockPeekCardContext ) => HTMLElement
816 *
817 * Where `body` is the `<span class="desktop-mode-dock-peek__card-body">`
818 * element that the peek would otherwise populate with ghosted
819 * content lines. The filter may:
820 * - Mutate `body` in place (e.g., append a custom child) and
821 * return it.
822 * - Empty `body` and append plugin-owned children.
823 * - Return an entirely different element to replace `body`.
824 *
825 * `detail.window` is the live `Window` instance the card represents
826 * — plugins can read `window.config`, call `window.getCurrentUrl()`,
827 * subscribe to lifecycle events, etc. `detail.item` is the dock
828 * item descriptor (id / title / icon / url).
829 *
830 * The filter is invoked under the `applyFilters` namespace
831 * `desktop-mode.dock.peek-card-content`.
832 *
833 * @since 0.6.2
834 */
835 DOCK_PEEK_CARD_CONTENT: "desktop-mode.dock.peek-card-content",
836 /**
837 * Filter, runs once per peek card right before it's appended to
838 * the popover. Receives the fully-built default card (with its
839 * mini-window chrome already populated) and can return either
840 * the same node, a mutated version, or an entirely different
841 * element to replace the card outright. Use this when the
842 * `peek-card-content` body filter isn't enough — e.g., when a
843 * plugin wants to swap the whole card chrome (custom titlebar,
844 * different shape) or wrap the card in a third-party component.
845 *
846 * Signature:
847 * ( card: HTMLElement, detail: DockPeekCardContext ) => HTMLElement
848 *
849 * If a plugin returns a brand-new node, it is responsible for
850 * preserving anything the peek relies on:
851 * - The `desktop-mode-dock-peek__card` class (used by the
852 * fan-out animation timing + hover styles).
853 * - A `click` handler if the card should still focus the
854 * window. The default click handler lives on the original
855 * node — replacing the node loses it.
856 *
857 * @since 0.6.2
858 */
859 DOCK_PEEK_CARD_ELEMENT: "desktop-mode.dock.peek-card-element",
860 // ------------------------------------------------------------------
861 // Overview / Arrange lifecycle actions.
862 //
863 // The "Arrange" admin-bar menu drives two layout algorithms —
864 // Cascade (instantly reposition every window in a staggered
865 // stack) and Overview (zoom-out grid view with click-to-focus).
866 // These hooks surface the state transitions so plugins can
867 // instrument analytics, apply custom transitions, override
868 // thumbnail decorations, etc. All actions; a filter for
869 // mutating the overview layout may be added later if plugins
870 // want to reorder or group thumbnails.
871 // ------------------------------------------------------------------
872 /** Action, fires before the overview enter animation starts. */
873 OVERVIEW_ENTERING: "desktop-mode.overview.entering",
874 /** Action, fires once the overview enter animation has completed. */
875 OVERVIEW_ENTERED: "desktop-mode.overview.entered",
876 /**
877 * Action, fires at the start of the overview-exit animation.
878 * Payload: `{ windowId?: string, reason: 'select' | 'cancel' }` —
879 * `windowId` set when the user clicked a thumbnail (reason
880 * 'select'); omitted when the user pressed Escape or clicked
881 * the backdrop (reason 'cancel').
882 */
883 OVERVIEW_EXITING: "desktop-mode.overview.exiting",
884 /** Action, fires once the overview-exit animation has settled. */
885 OVERVIEW_EXITED: "desktop-mode.overview.exited",
886 /** Action, fires when the cursor enters a thumbnail. Payload `{ windowId }`. */
887 OVERVIEW_WINDOW_HOVER: "desktop-mode.overview.window-hover",
888 /** Action, fires when the cursor leaves a thumbnail. Payload `{ windowId }`. */
889 OVERVIEW_WINDOW_UNHOVER: "desktop-mode.overview.window-unhover",
890 /** Action, fires the instant a thumbnail click is registered (before exit + maximize kick in). Payload `{ windowId }`. */
891 OVERVIEW_WINDOW_CLICK: "desktop-mode.overview.window-click",
892 /** Action, fires before cascade computes + applies new positions. Payload `{ windowCount }`. */
893 ARRANGE_CASCADE_STARTING: "desktop-mode.arrange.cascade.starting",
894 /** Action, fires after cascade has positioned every window. Payload `{ windowCount }`. */
895 ARRANGE_CASCADE_APPLIED: "desktop-mode.arrange.cascade.applied",
896 /** Action, fires before tile computes + applies new positions. Payload `{ windowCount, cols, rows }`. */
897 ARRANGE_TILE_STARTING: "desktop-mode.arrange.tile.starting",
898 /** Action, fires after tile has positioned every window. Payload `{ windowCount, cols, rows }`. */
899 ARRANGE_TILE_APPLIED: "desktop-mode.arrange.tile.applied",
900 /**
901 * Filter on the tile-grid dimensions chosen by the built-in
902 * algorithm. Receives `{ cols, rows }` plus a context arg
903 * `{ windowCount, areaWidth, areaHeight }`. Plugins can return
904 * a different `{ cols, rows }` to enforce a custom layout
905 * (fixed-column newsroom, golden-ratio cells, etc.). Returned
906 * values are validated — non-positive integers, or a product
907 * smaller than `windowCount`, fall back to the original.
908 */
909 ARRANGE_TILE_DIMENSIONS: "desktop-mode.arrange.tile.dimensions",
910 /** Action, fires when snap-to-grid is toggled. Payload `{ enabled }`. */
911 ARRANGE_SNAP_CHANGED: "desktop-mode.arrange.snap.changed",
912 /**
913 * Filter on the snap-grid cell size. Receives
914 * `{ cellWidth, cellHeight }` plus a context arg
915 * `{ areaWidth, areaHeight }`. Plugins can return different
916 * dimensions to enforce a Tetris-style fixed grid, a musical
917 * staff aspect, etc. Non-positive returns fall back to the
918 * original.
919 */
920 ARRANGE_SNAP_CELL_SIZE: "desktop-mode.arrange.snap.cell-size",
921 /**
922 * Action, fires when the user clicks a plugin-registered entry in
923 * the Arrange admin-bar submenu (items added via the
924 * `desktop_mode_arrange_menu_items` PHP filter). Payload `{ id }`
925 * where `id` is the item's `id` field as registered. Plugins
926 * subscribe here to run their custom arrangement logic.
927 */
928 ARRANGE_CUSTOM_ACTION: "desktop-mode.arrange.custom-action",
929 // ------------------------------------------------------------------
930 // Snap-zones — Windows-style edge snapping with a split-overview
931 // picker to fill the opposite half after commit.
932 // ------------------------------------------------------------------
933 /**
934 * Action, fires when the drag cursor enters a snap zone and the
935 * shell shows the target-position preview. Payload
936 * `{ windowId, zone: 'left' | 'right' }`.
937 */
938 SNAP_ZONE_PENDING: "desktop-mode.snap.zone-pending",
939 /**
940 * Action, fires when the drag cursor leaves the snap zone without
941 * releasing — the preview disappears. Payload `{ windowId }`.
942 */
943 SNAP_ZONE_CANCELED: "desktop-mode.snap.zone-canceled",
944 /**
945 * Action, fires once the window has animated into its snapped
946 * bounds. Payload `{ windowId, zone: 'left' | 'right' }`.
947 */
948 SNAP_ZONE_COMMITTED: "desktop-mode.snap.zone-committed",
949 /**
950 * Action, fires when a user picks a thumbnail from the split
951 * overview to fill the opposite half. Payload
952 * `{ windowId, zone: 'left' | 'right' }`.
953 */
954 SNAP_SPLIT_FILLED: "desktop-mode.snap.split-filled",
955 // ------------------------------------------------------------------
956 // Widgets — the right-side column. Widgets paint above the
957 // wallpaper but beneath windows. Lifecycle mirrors canvas
958 // wallpapers: register via filter, mount/unmount actions bracket
959 // each paint, mount-failed fires on sync throws / async rejects.
960 // ------------------------------------------------------------------
961 /** Filter, receives the widget registry array. */
962 WIDGETS: "desktop-mode.widgets",
963 /** Action before a widget mounts. Payload `{ id, container, ctx }`. */
964 WIDGET_MOUNTING: "desktop-mode.widget.mounting",
965 /** Action after a widget mounts successfully. Payload `{ id, container, ctx }`. */
966 WIDGET_MOUNTED: "desktop-mode.widget.mounted",
967 /** Action before a widget tears down. Payload `{ id }`. */
968 WIDGET_UNMOUNTING: "desktop-mode.widget.unmounting",
969 /** Action when a widget's mount throws / rejects. Payload `{ id, error }`. */
970 WIDGET_MOUNT_FAILED: "desktop-mode.widget.mount-failed",
971 /** Action when the user adds a widget via the picker. Payload `{ id }`. */
972 WIDGET_ADDED: "desktop-mode.widget.added",
973 /** Action when the user removes a widget via the card's × button. Payload `{ id }`. */
974 WIDGET_REMOVED: "desktop-mode.widget.removed",
975 // ------------------------------------------------------------------
976 // Virtual-desktop ("Spaces") lifecycle actions.
977 //
978 // Spaces let users group windows into separate workspaces and flip
979 // between them from the overview top bar. These hooks expose every
980 // state change so plugins can persist per-space state, sync custom
981 // indicators, or react to the user's workspace context.
982 // ------------------------------------------------------------------
983 /** Action, fires when a new desktop is created. Payload `{ desktopId }`. */
984 DESKTOP_CREATED: "desktop-mode.desktop.created",
985 /** Action, fires when a desktop is closed. Payload `{ desktopId, migratedTo }`. */
986 DESKTOP_CLOSED: "desktop-mode.desktop.closed",
987 /** Action, fires when the active desktop changes. Payload `{ from, to }`. */
988 DESKTOP_SWITCHED: "desktop-mode.desktop.switched",
989 /**
990 * Filter. Returns the id of the "primary" desktop — the one the
991 * shell treats as canonical for batch operations. Receives the
992 * default (first desktop's id) and the full `Desktop[]` list.
993 * @since 0.5.0
994 */
995 PRIMARY_DESKTOP_ID: "desktop-mode.primary-desktop-id",
996 // ------------------------------------------------------------------
997 // Batch window operations.
998 // ------------------------------------------------------------------
999 /**
1000 * Action, fires before {@link WindowManager.closeAll} starts
1001 * iterating. Payload `{ candidates: Window[] }` — every window the
1002 * shell is about to close (after `exceptIds` was applied).
1003 * @since 0.5.0
1004 */
1005 WINDOWS_BEFORE_CLOSE_ALL: "desktop-mode.windows.before-close-all",
1006 /**
1007 * Filter, runs inside {@link WindowManager.closeAll}. Receives the
1008 * candidate `Window[]` list and returns the (possibly trimmed) list
1009 * that will actually be closed. Plugins use this to PROTECT specific
1010 * windows from a bulk close — e.g. keep the active draft open.
1011 * Returning an empty array cancels the close entirely.
1012 * @since 0.5.0
1013 */
1014 WINDOWS_CLOSE_ALL: "desktop-mode.windows.close-all",
1015 /**
1016 * Action, fires after {@link WindowManager.closeAll} has finished.
1017 * Payload `{ closed: number, skipped: Window[] }`.
1018 * @since 0.5.0
1019 */
1020 WINDOWS_AFTER_CLOSE_ALL: "desktop-mode.windows.after-close-all",
1021 // ------------------------------------------------------------------
1022 // Slash-command lifecycle.
1023 // ------------------------------------------------------------------
1024 /**
1025 * Filter. Runs immediately before a command's `run()` is invoked.
1026 * Receives `{ proceed: true, slug, args, command }` and may return
1027 * the same shape with `proceed: false` to cancel the run.
1028 * @since 0.5.0
1029 */
1030 COMMAND_BEFORE_RUN: "desktop-mode.command.before-run",
1031 /**
1032 * Action, fires after a command's `run()` resolves successfully.
1033 * Payload `{ slug, args, command, result }`.
1034 * @since 0.5.0
1035 */
1036 COMMAND_AFTER_RUN: "desktop-mode.command.after-run",
1037 /**
1038 * Action, fires when a command's `run()` throws. Payload
1039 * `{ slug, args, command, error }`.
1040 * @since 0.5.0
1041 */
1042 COMMAND_ERROR: "desktop-mode.command.error",
1043 // ------------------------------------------------------------------
1044 // Shell-level lifecycle actions.
1045 // ------------------------------------------------------------------
1046 /**
1047 * Action, fires (debounced) after the browser viewport stops
1048 * resizing. Payload `{ width, height }` describes the shell's
1049 * bounding rect — plugins that render canvas-driven UIs hook here
1050 * to adjust their render surface.
1051 */
1052 SHELL_RESIZED: "desktop-mode.shell.resized",
1053 /**
1054 * Action mirroring `document.visibilitychange` for the shell as a
1055 * whole. Payload `{ state: 'visible' | 'hidden' }`. Different from
1056 * the wallpaper-specific visibility action in that it fires
1057 * regardless of which wallpaper (if any) is active.
1058 */
1059 SHELL_VISIBILITY: "desktop-mode.shell.visibility",
1060 /**
1061 * Action — fires when a `wp.desktop.connect()` connection
1062 * completes its iframe handshake. Payload:
1063 * `{ connectionId, targetWindowId, topics }`.
1064 *
1065 * @since 0.5.2
1066 */
1067 CONNECTION_OPENED: "desktop-mode.connection.opened",
1068 /**
1069 * Action — fires when a connection tears down. Payload:
1070 * `{ connectionId, reason: 'disconnect' | 'window-closed' | 'navigated' }`.
1071 *
1072 * @since 0.5.2
1073 */
1074 CONNECTION_CLOSED: "desktop-mode.connection.closed",
1075 /**
1076 * Action — fires for every message routed through a connection.
1077 * Payload: `{ connectionId, topic, direction: 'in' | 'out' }`.
1078 * Used for debug consoles + traffic auditing; high-volume topics
1079 * fire this many times per second, so subscribers should be
1080 * cheap.
1081 *
1082 * @since 0.5.2
1083 */
1084 CONNECTION_MESSAGE: "desktop-mode.connection.message",
1085 /**
1086 * Filter — fires when an iframe calls
1087 * `wp.desktop.iframe.requestConnection()`. Default value is
1088 * `true` (accept). Return `false` to reject, or an object
1089 * `{ topics: string[] }` to accept while narrowing the topic
1090 * list. `$context` carries `{ windowId, requestId, topics }`.
1091 *
1092 * @since 0.5.2
1093 */
1094 IFRAME_CONNECTION_REQUEST: "desktop-mode.iframe.connection-request",
1095 // ------------------------------------------------------------------
1096 // Window content relations & link renderers (since 0.9.4). A window
1097 // may carry a content identity ("I am comment 45 of post 123");
1098 // windows resolving to the same root form a relation group, and a
1099 // pluggable renderer draws the ties on the desktop. Engine:
1100 // `src/window-links/engine.ts`; registry:
1101 // `src/window-links/renderer-registry.ts`. See
1102 // `docs/examples/window-links.md`.
1103 // ------------------------------------------------------------------
1104 /**
1105 * Action — fires when a window's content identity is set, replaced,
1106 * or cleared. Payload: `{ windowId: string, content:
1107 * WindowContentRef | null, previous: WindowContentRef | null,
1108 * source: 'config' | 'bridge' | 'api' }`. The matching
1109 * `desktop-mode-window-content-changed` CustomEvent dispatches on
1110 * `document` with the same payload.
1111 *
1112 * @since 0.9.4
1113 */
1114 WINDOW_CONTENT_CHANGED: "desktop-mode.window-links.content-changed",
1115 /**
1116 * Action — fires when relation-group MEMBERSHIP changes (a window
1117 * gained/lost an identity, or a member window opened/closed).
1118 * Payload: `{ groups: WindowLinkGroup[] }`. Deliberately NOT fired
1119 * on move/resize (renderers get live geometry through their frame
1120 * subscription) nor on focus-recency reordering. The matching
1121 * `desktop-mode-window-link-groups-changed` CustomEvent dispatches
1122 * on `document` with the same payload.
1123 *
1124 * @since 0.9.4
1125 */
1126 WINDOW_LINK_GROUPS_CHANGED: "desktop-mode.window-links.groups-changed",
1127 /**
1128 * Filter — applied to every content identity as it is set, before
1129 * storage. Signature: `( ref: WindowContentRef | null, ctx: {
1130 * windowId: string, source: 'config' | 'bridge' | 'api' } ) =>
1131 * WindowContentRef | null`. Return `null` to suppress the identity,
1132 * or a rewritten ref to remap it (e.g. point a custom object type
1133 * at your own root scheme).
1134 *
1135 * @since 0.9.4
1136 */
1137 WINDOW_LINKS_CONTENT: "desktop-mode.window-links.content",
1138 /**
1139 * Filter — applied to the computed relation-group list on every
1140 * read (`wp.desktop.relations.groups()`). Signature:
1141 * `( groups: WindowLinkGroup[] ) => WindowLinkGroup[]`. Merge,
1142 * split, or inject groups here.
1143 *
1144 * @since 0.9.4
1145 */
1146 WINDOW_LINK_GROUPS: "desktop-mode.window-links.groups",
1147 /**
1148 * Filter — applied to the derived directed-edge list on every read
1149 * (`wp.desktop.relations.edges()`). Signature: `( edges:
1150 * WindowLinkEdge[] ) => WindowLinkEdge[]` where each edge is
1151 * `{ fromWindowId, toWindowId, kind: 'child-root' | 'reference',
1152 * bidirectional }`. Add, drop, or redirect ties here — this is
1153 * what the render host feeds to the active renderer.
1154 *
1155 * @since 0.9.4
1156 */
1157 WINDOW_LINK_EDGES: "desktop-mode.window-links.edges",
1158 /**
1159 * Filter — applied to the related-entity navigation items resolved
1160 * for a window, every time the title bar's "Related" button decides
1161 * its visibility and every time its menu is built. Signature:
1162 * `( items: RelatedEntityItem[], ctx: { windowId: string, content:
1163 * WindowContentRef | null } ) => RelatedEntityItem[]` where each
1164 * item is `{ id, group, label, url, groupLabel?, icon?, count? }`.
1165 * The unfiltered list is whatever the window's content identity
1166 * carried in `related` (built server-side; see the
1167 * `desktop_mode_window_related_entities` PHP filter). Add, drop, or
1168 * relabel items here — return an empty array to hide the button.
1169 *
1170 * @since 0.9.6
1171 */
1172 RELATED_ENTITIES_ITEMS: "desktop-mode.related-entities.items",
1173 /**
1174 * Filter — applied to the registered window-link renderer list on
1175 * every read (`wp.desktop.listWindowLinkRenderers()`). Signature:
1176 * `( defs: WindowLinkRendererDef[] ) => WindowLinkRendererDef[]`.
1177 *
1178 * @since 0.9.4
1179 */
1180 WINDOW_LINK_RENDERERS: "desktop-mode.window-links.renderers",
1181 /**
1182 * Filter — applied to the resolved ACTIVE renderer id after the OS
1183 * Settings selection is read, before the registry lookup.
1184 * Signature: `( id: string ) => string`. Return a different
1185 * registered id (or `'none'`) to force-swap the renderer without
1186 * touching the user's setting.
1187 *
1188 * @since 0.9.4
1189 */
1190 WINDOW_LINK_RENDERER: "desktop-mode.window-links.renderer",
1191 // ------------------------------------------------------------------
1192 // OS-file drop manager (since 0.30.0). Catches files dragged from
1193 // the user's host OS (Finder / Explorer / Nautilus) onto any
1194 // desktop-mode surface and routes them through a confirmation
1195 // dialog before uploading to the Media Library. Authoritative
1196 // constants live in `src/os-file-drop/hooks.ts`; mirrored here so
1197 // every hook the shell fires is reachable from a single `HOOKS`
1198 // import. See `docs/examples/os-file-drop.md`.
1199 // ------------------------------------------------------------------
1200 /** Filter — `(files: File[], ctx) => File[]`, before mime/size check. */
1201 FILE_DROP_FILES_DETECTED: "desktop-mode.drop.files-detected",
1202 /** Action — `{ rejections, context }` for files that failed policy. */
1203 FILE_DROP_FILES_REJECTED: "desktop-mode.drop.files-rejected",
1204 /** Filter — `(entry, ctx) => entry`, per-file dialog defaults. */
1205 FILE_DROP_DIALOG_FIELDS: "desktop-mode.drop.dialog-fields",
1206 /** Filter — `(payload, ctx) => payload | null`, last call before POST. */
1207 FILE_DROP_BEFORE_UPLOAD: "desktop-mode.drop.before-upload",
1208 /** Action — `{ file, fields, context, abort }` once XHR is open and about to send. @since 0.31.0 */
1209 FILE_DROP_UPLOAD_STARTED: "desktop-mode.drop.upload-started",
1210 /** Action — `{ file, fields, context, loaded, total, indeterminate }` per progress tick. @since 0.31.0 */
1211 FILE_DROP_UPLOAD_PROGRESS: "desktop-mode.drop.upload-progress",
1212 /** Action — `{ file, result, fields, context }` after successful upload. `file` since 0.31.0. */
1213 FILE_DROP_AFTER_UPLOAD: "desktop-mode.drop.after-upload",
1214 /** Action — `{ file, error, context }` on upload failure. */
1215 FILE_DROP_UPLOAD_FAILED: "desktop-mode.drop.upload-failed",
1216 // ------------------------------------------------------------------
1217 // Session / authentication (since 0.9.8). Fired by
1218 // `src/auth-recovery/index.ts` when the WordPress login session
1219 // expires and when it comes back. Mirrored as document
1220 // CustomEvents (`desktop-mode-auth-lost` / `-restored`) for
1221 // listeners outside the hook bus.
1222 // ------------------------------------------------------------------
1223 /**
1224 * Action, no payload — the Heartbeat `wp-auth-check` flag
1225 * reported the session as expired. Fires once per outage.
1226 * Pause pollers / mutations here; requests made while the
1227 * session is down will 401.
1228 *
1229 * @since 0.9.8
1230 */
1231 AUTH_LOST: "desktop-mode.auth.lost",
1232 /**
1233 * Action, no payload — the session is authenticated again and
1234 * the shell's cached nonces have been (or are about to be, same
1235 * tick) refreshed in place. Resume pollers and re-fetch any
1236 * state that may have failed during the outage. May fire
1237 * without a preceding `AUTH_LOST` when re-auth was detected
1238 * from an iframe or another browser tab before the shell's own
1239 * heartbeat noticed the expiry.
1240 *
1241 * @since 0.9.8
1242 */
1243 AUTH_RESTORED: "desktop-mode.auth.restored"
1244 };
1245 let _whenReadySeq = 0;
1246 function whenReady(cb) {
1247 if (didAction(HOOKS.INIT) > 0) {
1248 Promise.resolve().then(cb);
1249 return;
1250 }
1251 const ns = `desktop-mode/when-ready-${++_whenReadySeq}`;
1252 addAction(HOOKS.INIT, ns, cb);
1253 }
1254 function isReady() {
1255 return didAction(HOOKS.INIT) > 0;
1256 }
1257 const IDENTITY_PARAMS = [
1258 "post_type",
1259 "page",
1260 "taxonomy",
1261 // WooCommerce (and other React-app-style plugins) register
1262 // SEPARATE top-level admin menus that all share `?page=wc-admin`
1263 // and only differ by `path` (e.g. `path=/analytics/overview`,
1264 // `path=/marketing`). Without `path` in the identity set, every
1265 // such menu collapses to the same window id — opening any one of
1266 // them lights up the dock indicator for ALL of them. WC's
1267 // /admin/path query is the most prominent example today; future
1268 // plugins that route inside `admin.php?page=` via a custom param
1269 // can either piggyback on `path` or grow this list.
1270 "path",
1271 // The post ID on `post.php?post=X&action=edit`. Without this, every
1272 // individual post edit URL collapses to `post-php`, so clicking a
1273 // second row in the Posts window just refocuses the first post's
1274 // window instead of opening the new one.
1275 "post",
1276 // The comment ID on `comment.php?action=editcomment&c=X` — the exact
1277 // analogue of `post` above. Without it every comment-edit URL
1278 // collapses to `comment-php`, so opening a second comment replaces
1279 // the first comment's window instead of opening its own (and the
1280 // window-links ties can only ever point at one comment at a time).
1281 "c",
1282 // NOTE: the generic `id` param is deliberately NOT identity.
1283 // Plugin list screens use `admin.php?page=foo&action=…&id=N` for
1284 // row actions; treating `id` as identity would open every such
1285 // action in a NEW window instead of navigating the list in place.
1286 // The cost: two entities of the same `admin.php?page=` screen
1287 // (e.g. two WooCommerce HPOS orders) can't be open side by side —
1288 // plugins that want that can differentiate via `path` or their own
1289 // window ids.
1290 // The term ID on `term.php?taxonomy=category&tag_ID=X` — the term
1291 // analogue of `post`. Without it every term-edit URL of the same
1292 // taxonomy collapses to one window, so opening a second category
1293 // from a post's Related menu just refocuses the first term's
1294 // window instead of opening its own.
1295 "tag_ID",
1296 // The attachment ID on `upload.php?item=X` (Media Library grid
1297 // with the details modal open). Without it every deep-linked media
1298 // item collapses to the plain `upload-php` window, so opening a
1299 // second image from a post's Related menu refocuses the first.
1300 "item",
1301 // Site-editor entity path: `site-editor.php?p=/wp_template_part/
1302 // twentytwentyfive//footer-columns`. Each template / template
1303 // part / pattern / navigation entity is a distinct "page" from
1304 // the user's perspective — picking "Header" after "Footer column"
1305 // should open a new window, not refocus the existing footer one.
1306 // Without `p` in identity, every site-editor URL collapses to
1307 // `site-editor-php` and the second pick is a no-op.
1308 "p"
1309 ];
1310 function slugify$1(path) {
1311 let decoded = path;
1312 try {
1313 decoded = decodeURIComponent(path);
1314 } catch {
1315 decoded = path;
1316 }
1317 return decoded.replace(/\.php/g, "-php").replace(/[?&=/]/g, "-").replace(/[^a-zA-Z0-9_-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "") || "index";
1318 }
1319 function deriveWindowId(url, adminUrl) {
1320 let parsed = null;
1321 try {
1322 parsed = new URL(url, adminUrl);
1323 } catch (err) {
1324 parsed = null;
1325 }
1326 if (parsed) {
1327 const basePath = new URL(adminUrl).pathname;
1328 const filename = parsed.pathname.replace(basePath, "").replace(/^\/+/, "");
1329 const significant = new URLSearchParams();
1330 for (const key of IDENTITY_PARAMS) {
1331 const value = parsed.searchParams.get(key);
1332 if (value) {
1333 significant.set(key, value);
1334 }
1335 }
1336 const query = significant.toString();
1337 return slugify$1(query ? `${filename}?${query}` : filename);
1338 }
1339 let path = url.replace(adminUrl, "");
1340 if (path.startsWith("/")) {
1341 path = path.substring(1);
1342 }
1343 return slugify$1(path);
1344 }
1345 function sanitizeClassName(value) {
1346 return value.replace(/[^a-zA-Z0-9_-]/g, "");
1347 }
1348 function applyTileEntryStagger(tile2) {
1349 tile2.style.setProperty(
1350 "--desktop-mode-file-tile-enter-delay",
1351 `${(Math.random() * 0.25).toFixed(3)}s`
1352 );
1353 tile2.style.setProperty(
1354 "--desktop-mode-file-tile-enter-duration",
1355 `${(0.3 + Math.random() * 0.25).toFixed(3)}s`
1356 );
1357 }
1358 function urlMatchKey(url) {
1359 try {
1360 const parsed = new URL(url, window.location.origin);
1361 parsed.searchParams.delete("desktop_mode_chromeless");
1362 parsed.searchParams.delete("desktop_mode_portal");
1363 return parsed.pathname.replace(/\/+$/, "") + "?" + parsed.searchParams.toString();
1364 } catch {
1365 return url;
1366 }
1367 }
1368 function urlReuseKey(url) {
1369 try {
1370 const parsed = new URL(url, window.location.origin);
1371 parsed.searchParams.delete("desktop_mode_chromeless");
1372 parsed.searchParams.delete("desktop_mode_portal");
1373 parsed.searchParams.delete("_wp_http_referer");
1374 parsed.searchParams.sort();
1375 return parsed.pathname.replace(/\/+$/, "") + "?" + parsed.searchParams.toString();
1376 } catch {
1377 return url;
1378 }
1379 }
1380 function sanitizeIconSvg(svg) {
1381 if (typeof svg !== "string" || svg === "") {
1382 return "";
1383 }
1384 if (typeof DOMParser === "undefined") {
1385 return "";
1386 }
1387 let doc;
1388 try {
1389 doc = new DOMParser().parseFromString(svg, "image/svg+xml");
1390 } catch {
1391 return "";
1392 }
1393 const root = doc.documentElement;
1394 if (!root || root.nodeName.toLowerCase() !== "svg") {
1395 return "";
1396 }
1397 if (doc.getElementsByTagName("parsererror").length > 0) {
1398 return "";
1399 }
1400 const BANNED_TAGS = /* @__PURE__ */ new Set(["script", "style", "foreignobject", "iframe", "object", "embed"]);
1401 const walk2 = (el) => {
1402 const children = Array.from(el.children);
1403 for (const child of children) {
1404 if (BANNED_TAGS.has(child.nodeName.toLowerCase())) {
1405 child.remove();
1406 continue;
1407 }
1408 for (const attr of Array.from(child.attributes)) {
1409 const name = attr.name.toLowerCase();
1410 const value = attr.value.trim().toLowerCase();
1411 if (name.startsWith("on")) {
1412 child.removeAttribute(attr.name);
1413 continue;
1414 }
1415 if (value.startsWith("javascript:")) {
1416 child.removeAttribute(attr.name);
1417 }
1418 }
1419 walk2(child);
1420 }
1421 };
1422 walk2(root);
1423 for (const attr of Array.from(root.attributes)) {
1424 const name = attr.name.toLowerCase();
1425 const value = attr.value.trim().toLowerCase();
1426 if (name.startsWith("on") || value.startsWith("javascript:")) {
1427 root.removeAttribute(attr.name);
1428 }
1429 }
1430 return root.outerHTML;
1431 }
1432 let inflight$1 = null;
1433 function isLoaded$1() {
1434 return !!window.desktopModeWindowSystem;
1435 }
1436 function injectScript$1(scriptUrl) {
1437 return new Promise((resolve2, reject) => {
1438 const existing = document.querySelector(
1439 'script[data-desktop-mode-window-system="1"]'
1440 );
1441 const finish = () => {
1442 if (isLoaded$1()) {
1443 resolve2();
1444 return;
1445 }
1446 reject(
1447 new Error(
1448 "[desktop-mode] window-system bundle loaded but did not register `window.desktopModeWindowSystem`."
1449 )
1450 );
1451 };
1452 if (existing) {
1453 if (isLoaded$1()) {
1454 finish();
1455 } else {
1456 existing.addEventListener("load", finish);
1457 existing.addEventListener(
1458 "error",
1459 () => reject(new Error("failed to load window-system bundle"))
1460 );
1461 }
1462 return;
1463 }
1464 const s = document.createElement("script");
1465 s.src = scriptUrl;
1466 s.async = true;
1467 s.dataset.desktopModeWindowSystem = "1";
1468 s.addEventListener("load", finish);
1469 s.addEventListener(
1470 "error",
1471 () => reject(new Error("failed to load window-system bundle"))
1472 );
1473 document.head.appendChild(s);
1474 });
1475 }
1476 function windowSystemBundleUrl() {
1477 const cfg = window.desktopModeConfig;
1478 return cfg?.windowSystemBundleUrl ?? "";
1479 }
1480 function preloadWindowSystem(scriptUrl) {
1481 if (!scriptUrl || isLoaded$1() || inflight$1) {
1482 return;
1483 }
1484 inflight$1 = injectScript$1(scriptUrl).catch((err) => {
1485 inflight$1 = null;
1486 if (typeof console !== "undefined") {
1487 console.warn(
1488 "[desktop-mode] window-system preload failed; will retry on first open():",
1489 err
1490 );
1491 }
1492 });
1493 }
1494 async function ensureWindowSystemLoaded(scriptUrl) {
1495 if (isLoaded$1()) {
1496 return window.desktopModeWindowSystem;
1497 }
1498 if (!scriptUrl) {
1499 const fn = window.desktopModeWindowSystem;
1500 if (fn) {
1501 return fn;
1502 }
1503 throw new Error(
1504 "[desktop-mode] ensureWindowSystemLoaded(): no bundle URL configured and `window.desktopModeWindowSystem` is not pre-registered."
1505 );
1506 }
1507 if (!inflight$1) {
1508 inflight$1 = injectScript$1(scriptUrl);
1509 }
1510 await inflight$1;
1511 return window.desktopModeWindowSystem;
1512 }
1513 const CANARY_TAG = "wpd-confirm-dialog";
1514 let inflight = null;
1515 function isLoaded() {
1516 return typeof window.customElements !== "undefined" && !!window.customElements.get(CANARY_TAG);
1517 }
1518 function injectScript(scriptUrl) {
1519 return new Promise((resolve2, reject) => {
1520 const existing = document.querySelector(
1521 'script[data-desktop-mode-shell-overlays="1"]'
1522 );
1523 const finish = () => {
1524 if (isLoaded()) {
1525 resolve2();
1526 return;
1527 }
1528 reject(
1529 new Error(
1530 "[desktop-mode] shell-overlays bundle loaded but did not register the overlay components."
1531 )
1532 );
1533 };
1534 if (existing) {
1535 if (isLoaded()) {
1536 finish();
1537 } else {
1538 existing.addEventListener("load", finish);
1539 existing.addEventListener(
1540 "error",
1541 () => reject(new Error("failed to load shell-overlays bundle"))
1542 );
1543 }
1544 return;
1545 }
1546 const s = document.createElement("script");
1547 s.src = scriptUrl;
1548 s.async = true;
1549 s.dataset.desktopModeShellOverlays = "1";
1550 s.addEventListener("load", finish);
1551 s.addEventListener(
1552 "error",
1553 () => reject(new Error("failed to load shell-overlays bundle"))
1554 );
1555 document.head.appendChild(s);
1556 });
1557 }
1558 function preloadShellOverlays(scriptUrl) {
1559 if (!scriptUrl || isLoaded() || inflight) {
1560 return;
1561 }
1562 inflight = injectScript(scriptUrl).catch((err) => {
1563 inflight = null;
1564 if (typeof console !== "undefined") {
1565 console.warn(
1566 "[desktop-mode] shell-overlays preload failed; will retry on first overlay use:",
1567 err
1568 );
1569 }
1570 });
1571 }
1572 function ensureShellOverlaysLoaded(scriptUrl) {
1573 if (isLoaded()) {
1574 return Promise.resolve();
1575 }
1576 if (!scriptUrl) {
1577 return Promise.resolve();
1578 }
1579 if (!inflight) {
1580 inflight = injectScript(scriptUrl);
1581 }
1582 return inflight;
1583 }
1584 function shellOverlaysBundleUrl() {
1585 const cfg = window.desktopModeConfig;
1586 return cfg?.shellOverlaysBundleUrl ?? "";
1587 }
1588 function openWithShellOverlays(isStillCurrent, fn) {
1589 const url = shellOverlaysBundleUrl();
1590 if (isLoaded() || !url) {
1591 fn();
1592 return;
1593 }
1594 void ensureShellOverlaysLoaded(url).then(() => {
1595 if (!isStillCurrent()) {
1596 return;
1597 }
1598 fn();
1599 }).catch((err) => {
1600 if (typeof console !== "undefined") {
1601 console.warn(
1602 "[desktop-mode] shell-overlays failed to load; menu/dialog suppressed:",
1603 err
1604 );
1605 }
1606 });
1607 }
1608 const TEXT_DOMAIN = "desktop-mode";
1609 function i18n() {
1610 return window.wp?.i18n;
1611 }
1612 function __(text, domain = TEXT_DOMAIN) {
1613 return i18n()?.__(text, domain) ?? text;
1614 }
1615 function _n(single, plural, number, domain = TEXT_DOMAIN) {
1616 return i18n()?._n(single, plural, number, domain) ?? (number === 1 ? single : plural);
1617 }
1618 function sprintf(format, ...args) {
1619 const impl = i18n()?.sprintf;
1620 if (impl) {
1621 return impl(format, ...args);
1622 }
1623 let i = 0;
1624 return format.replace(/%(?:(\d+)\$)?[sd]/g, (_match, pos) => {
1625 const idx = pos ? Number.parseInt(pos, 10) - 1 : i++;
1626 return String(args[idx] ?? "");
1627 });
1628 }
1629 function isValidGrid(candidate, windowCount) {
1630 if (!candidate || typeof candidate !== "object") {
1631 return false;
1632 }
1633 const c = candidate.cols;
1634 const r = candidate.rows;
1635 if (typeof c !== "number" || typeof r !== "number") {
1636 return false;
1637 }
1638 if (!Number.isFinite(c) || !Number.isFinite(r)) {
1639 return false;
1640 }
1641 if (c < 1 || r < 1) {
1642 return false;
1643 }
1644 return Math.floor(c) * Math.floor(r) >= windowCount;
1645 }
1646 function isValidCellSize(candidate) {
1647 if (!candidate || typeof candidate !== "object") {
1648 return false;
1649 }
1650 const w = candidate.cellWidth;
1651 const h = candidate.cellHeight;
1652 if (typeof w !== "number" || typeof h !== "number") {
1653 return false;
1654 }
1655 if (!Number.isFinite(w) || !Number.isFinite(h)) {
1656 return false;
1657 }
1658 return w > 0 && h > 0;
1659 }
1660 function pickGridDimensions(n, width, height) {
1661 if (n <= 1) {
1662 return { cols: 1, rows: 1 };
1663 }
1664 const areaAspect = width / Math.max(1, height);
1665 const max = 6;
1666 let best = { cols: n, rows: 1, score: Infinity };
1667 for (let cols = 1; cols <= Math.min(max, n); cols++) {
1668 const rows = Math.min(max, Math.ceil(n / cols));
1669 if (cols * rows < n) {
1670 continue;
1671 }
1672 const cellAspect = width / cols / Math.max(1, height / rows);
1673 const aspectDelta = Math.abs(cellAspect - areaAspect);
1674 const emptyCells = cols * rows - n;
1675 const score = aspectDelta + emptyCells * 0.05;
1676 if (score < best.score) {
1677 best = { cols, rows, score };
1678 }
1679 }
1680 return { cols: best.cols, rows: best.rows };
1681 }
1682 function computeOverviewLayout(windows, rect, topInset = 0) {
1683 const n = windows.length;
1684 if (n === 0) {
1685 return [];
1686 }
1687 const cols = Math.ceil(Math.sqrt(n));
1688 const rows = Math.ceil(n / cols);
1689 const padding = 40;
1690 const gap = 24;
1691 const labelReserve = 34;
1692 const cellWidth = (rect.width - padding * 2 - gap * (cols - 1)) / cols;
1693 const cellHeight = (rect.height - padding * 2 - topInset - gap * (rows - 1)) / rows;
1694 const thumbCellHeight = Math.max(40, cellHeight - labelReserve);
1695 return windows.map((win, i) => {
1696 const col = i % cols;
1697 const row = Math.floor(i / cols);
1698 const cellX = rect.left + padding + col * (cellWidth + gap);
1699 const cellY = rect.top + topInset + padding + row * (cellHeight + gap) + labelReserve;
1700 const sourceW = win.element.offsetWidth;
1701 const sourceH = win.element.offsetHeight;
1702 const scale = Math.min(
1703 cellWidth / sourceW,
1704 thumbCellHeight / sourceH
1705 );
1706 const scaledW = sourceW * scale;
1707 const scaledH = sourceH * scale;
1708 return {
1709 win,
1710 x: cellX + (cellWidth - scaledW) / 2,
1711 y: cellY + (thumbCellHeight - scaledH) / 2,
1712 scale
1713 };
1714 });
1715 }
1716 const OVERVIEW_TOP_BAR_RESERVE = 120;
1717 const OVERVIEW_INERT_ELEMENTS = [
1718 "adminmenumain",
1719 "adminmenuback",
1720 "desktop-mode-dock",
1721 "desktop-mode-side-dock",
1722 "desktop-mode-widgets"
1723 ];
1724 function inertWpBodyContentChildren(inactive) {
1725 const content = document.getElementById("wpbody-content");
1726 if (!content) {
1727 return;
1728 }
1729 for (const child of Array.from(content.children)) {
1730 child.inert = inactive;
1731 }
1732 }
1733 function inertWindowChildren(mgr, inactive) {
1734 for (const w of mgr._stack) {
1735 for (const child of Array.from(w.element.children)) {
1736 child.inert = inactive;
1737 }
1738 }
1739 }
1740 function enterOverview(mgr) {
1741 if (mgr._overviewActive) {
1742 return;
1743 }
1744 const onActive = mgr._stack.filter(
1745 (w) => w.config.desktopId === mgr._activeDesktopId
1746 );
1747 if (onActive.length > 0 && onActive.every((w) => w.state === "minimized")) {
1748 for (const w of onActive) {
1749 try {
1750 w.restore();
1751 } catch (err) {
1752 if (typeof console !== "undefined") {
1753 console.error(
1754 "[desktop-mode] enterOverview: window.restore() threw for",
1755 w.id,
1756 err
1757 );
1758 }
1759 }
1760 }
1761 }
1762 const eligible = mgr._stack.filter(
1763 (w) => w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
1764 );
1765 mgr._overviewActive = true;
1766 doAction(HOOKS.OVERVIEW_ENTERING, {});
1767 for (const id of OVERVIEW_INERT_ELEMENTS) {
1768 const el = document.getElementById(id);
1769 if (el) {
1770 el.inert = true;
1771 }
1772 }
1773 inertWpBodyContentChildren(true);
1774 inertWindowChildren(mgr, true);
1775 mgr._overviewSnapshot.clear();
1776 for (const w of eligible) {
1777 mgr._overviewSnapshot.set(w.id, {
1778 transform: w.element.style.transform || "",
1779 transition: w.element.style.transition || ""
1780 });
1781 }
1782 for (const w of eligible) {
1783 if (w.state === "fullscreen") {
1784 w.toggleFullscreen();
1785 }
1786 }
1787 const currentRect = mgr._desktop.getBoundingClientRect();
1788 const docks = Array.from(
1789 document.querySelectorAll(".desktop-mode-dock")
1790 );
1791 let reclaimedWidth = 0;
1792 for (const d of docks) {
1793 const r = d.getBoundingClientRect();
1794 const verticallyOverlaps = r.bottom > currentRect.top && r.top < currentRect.bottom;
1795 const isHorizontalRail = r.height > r.width;
1796 if (verticallyOverlaps && isHorizontalRail) {
1797 reclaimedWidth += r.width;
1798 }
1799 }
1800 const targetRect = new DOMRect(
1801 0,
1802 0,
1803 currentRect.width + reclaimedWidth,
1804 currentRect.height
1805 );
1806 mgr._desktop.classList.add("desktop-mode-area--overview");
1807 const shell = document.getElementById("desktop-mode-shell");
1808 shell?.classList.add("desktop-mode-shell--overview");
1809 mgr._overviewTopBar = buildOverviewTopBar(mgr);
1810 mgr._desktop.appendChild(mgr._overviewTopBar);
1811 const layout = computeOverviewLayout(
1812 eligible,
1813 targetRect,
1814 OVERVIEW_TOP_BAR_RESERVE
1815 );
1816 mgr._overviewLabels.clear();
1817 for (const item of layout) {
1818 const el = item.win.element;
1819 el.classList.add("desktop-mode-window--overview");
1820 const dx = item.x - el.offsetLeft;
1821 const dy = item.y - el.offsetTop;
1822 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
1823 const label = createOverviewLabel(item);
1824 el.insertAdjacentElement("afterend", label);
1825 mgr._overviewLabels.set(item.win.id, label);
1826 }
1827 const pressTargetForEvent = (e) => {
1828 const target2 = e.target;
1829 const winEl = target2?.closest(
1830 ".desktop-mode-window--overview"
1831 );
1832 if (winEl) {
1833 return {
1834 id: winEl.id.replace(/^wp-window-/, ""),
1835 element: winEl
1836 };
1837 }
1838 if (target2 === mgr._desktop) {
1839 return { id: "backdrop", element: mgr._desktop };
1840 }
1841 return null;
1842 };
1843 mgr._overviewPointerDownHandler = (e) => {
1844 if (e.button !== 0) {
1845 mgr._overviewPressTarget = null;
1846 return;
1847 }
1848 mgr._overviewPressTarget = pressTargetForEvent(e);
1849 if (mgr._overviewPressTarget) {
1850 e.preventDefault();
1851 e.stopPropagation();
1852 }
1853 };
1854 mgr._overviewPointerUpHandler = (e) => {
1855 if (e.button !== 0) {
1856 return;
1857 }
1858 const pressed = mgr._overviewPressTarget;
1859 mgr._overviewPressTarget = null;
1860 if (!pressed) {
1861 return;
1862 }
1863 const rect = pressed.element.getBoundingClientRect();
1864 const inside = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
1865 if (!inside) {
1866 return;
1867 }
1868 e.preventDefault();
1869 e.stopPropagation();
1870 if (pressed.id === "backdrop") {
1871 exitOverview(mgr);
1872 return;
1873 }
1874 const selected = mgr.getById(pressed.id);
1875 doAction(HOOKS.OVERVIEW_WINDOW_CLICK, { windowId: pressed.id });
1876 exitOverview(mgr, selected);
1877 };
1878 mgr._overviewKeyHandler = (e) => {
1879 if (e.key === "Escape") {
1880 exitOverview(mgr);
1881 return;
1882 }
1883 if (e.key === "Enter") {
1884 const target2 = e.target;
1885 const doc = target2?.ownerDocument || document;
1886 if (doc.activeElement && doc.activeElement.tagName === "BUTTON") {
1887 return;
1888 }
1889 e.preventDefault();
1890 if (mgr._overviewAddTileFocused) {
1891 commitAddTile(mgr);
1892 return;
1893 }
1894 exitOverview(mgr);
1895 }
1896 };
1897 mgr._desktop.addEventListener(
1898 "pointerdown",
1899 mgr._overviewPointerDownHandler,
1900 true
1901 );
1902 mgr._desktop.addEventListener(
1903 "pointerup",
1904 mgr._overviewPointerUpHandler,
1905 true
1906 );
1907 mgr._overviewClickBlocker = (e) => {
1908 const target2 = e.target;
1909 if (target2?.closest(".desktop-mode-overview-top-bar")) {
1910 return;
1911 }
1912 e.stopPropagation();
1913 e.preventDefault();
1914 };
1915 mgr._desktop.addEventListener(
1916 "click",
1917 mgr._overviewClickBlocker,
1918 true
1919 );
1920 document.addEventListener("keydown", mgr._overviewKeyHandler);
1921 mgr._lastOverviewHoverId = null;
1922 mgr._overviewMouseHandler = (e) => {
1923 const target2 = e.target;
1924 const winEl = target2?.closest(
1925 ".desktop-mode-window--overview"
1926 );
1927 const newId = winEl ? winEl.id.replace(/^wp-window-/, "") : null;
1928 if (newId === mgr._lastOverviewHoverId) {
1929 return;
1930 }
1931 if (mgr._lastOverviewHoverId) {
1932 doAction(HOOKS.OVERVIEW_WINDOW_UNHOVER, {
1933 windowId: mgr._lastOverviewHoverId
1934 });
1935 }
1936 if (newId) {
1937 doAction(HOOKS.OVERVIEW_WINDOW_HOVER, { windowId: newId });
1938 }
1939 mgr._lastOverviewHoverId = newId;
1940 };
1941 mgr._desktop.addEventListener("mouseover", mgr._overviewMouseHandler);
1942 mgr._overviewEnterTimeoutId = window.setTimeout(() => {
1943 mgr._overviewEnterTimeoutId = null;
1944 if (mgr._overviewActive) {
1945 doAction(HOOKS.OVERVIEW_ENTERED, {});
1946 }
1947 }, 300);
1948 }
1949 function cancelOverviewTimers(mgr) {
1950 if (mgr._overviewEnterTimeoutId !== null) {
1951 window.clearTimeout(mgr._overviewEnterTimeoutId);
1952 mgr._overviewEnterTimeoutId = null;
1953 }
1954 if (mgr._overviewExitTimeoutId !== null) {
1955 window.clearTimeout(mgr._overviewExitTimeoutId);
1956 mgr._overviewExitTimeoutId = null;
1957 }
1958 }
1959 function buildOverviewTopBar(mgr) {
1960 const bar = document.createElement("div");
1961 bar.className = "desktop-mode-overview-top-bar";
1962 const list2 = document.createElement("div");
1963 list2.className = "desktop-mode-overview-top-bar__list";
1964 bar.appendChild(list2);
1965 for (const d of mgr._desktops) {
1966 list2.appendChild(buildDesktopTile(mgr, d));
1967 }
1968 const addTile = document.createElement("button");
1969 addTile.type = "button";
1970 addTile.className = "desktop-mode-overview-top-bar__tile desktop-mode-overview-top-bar__tile--add";
1971 if (mgr._overviewAddTileFocused) {
1972 addTile.classList.add(
1973 "desktop-mode-overview-top-bar__tile--cursor"
1974 );
1975 }
1976 addTile.setAttribute("aria-label", __("Add new desktop"));
1977 addTile.innerHTML = '<span class="desktop-mode-overview-top-bar__tile-plus" aria-hidden="true">+</span>';
1978 addTile.addEventListener("click", (e) => {
1979 e.preventDefault();
1980 e.stopPropagation();
1981 commitAddTile(mgr);
1982 });
1983 list2.appendChild(addTile);
1984 return bar;
1985 }
1986 function commitAddTile(mgr) {
1987 const created = createDesktop(mgr);
1988 mgr._overviewAddTileFocused = false;
1989 exitOverviewToDesktop(mgr, created.id);
1990 }
1991 function buildDesktopTile(mgr, d) {
1992 const wrapper = document.createElement("div");
1993 wrapper.className = "desktop-mode-overview-top-bar__tile-wrapper";
1994 const tile2 = document.createElement("button");
1995 tile2.type = "button";
1996 tile2.className = "desktop-mode-overview-top-bar__tile";
1997 tile2.dataset.desktopId = d.id;
1998 if (d.id === mgr._activeDesktopId && !mgr._overviewAddTileFocused) {
1999 tile2.classList.add("desktop-mode-overview-top-bar__tile--active");
2000 }
2001 tile2.setAttribute("aria-label", sprintf(__("Switch to %s"), d.label));
2002 const preview = document.createElement("span");
2003 preview.className = "desktop-mode-overview-top-bar__tile-preview";
2004 const count = mgr._stack.filter(
2005 (w) => w.config.desktopId === d.id
2006 ).length;
2007 if (count > 0) {
2008 const badge = document.createElement("span");
2009 badge.className = "desktop-mode-overview-top-bar__tile-count";
2010 badge.textContent = String(count);
2011 preview.appendChild(badge);
2012 }
2013 tile2.appendChild(preview);
2014 const label = document.createElement("span");
2015 label.className = "desktop-mode-overview-top-bar__tile-label";
2016 label.textContent = d.label;
2017 tile2.appendChild(label);
2018 tile2.addEventListener("click", (e) => {
2019 e.preventDefault();
2020 e.stopPropagation();
2021 exitOverviewToDesktop(mgr, d.id);
2022 });
2023 const closeBtn = document.createElement("button");
2024 closeBtn.type = "button";
2025 closeBtn.className = "desktop-mode-overview-top-bar__tile-close";
2026 closeBtn.setAttribute("aria-label", sprintf(__("Close %s"), d.label));
2027 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>';
2028 closeBtn.addEventListener("click", (e) => {
2029 e.preventDefault();
2030 e.stopPropagation();
2031 closeDesktop(mgr, d.id);
2032 refreshOverviewTopBar(mgr);
2033 });
2034 wrapper.appendChild(tile2);
2035 wrapper.appendChild(closeBtn);
2036 return wrapper;
2037 }
2038 function refreshOverviewTopBar(mgr) {
2039 if (!mgr._overviewTopBar) {
2040 return;
2041 }
2042 const fresh = buildOverviewTopBar(mgr);
2043 mgr._overviewTopBar.replaceWith(fresh);
2044 mgr._overviewTopBar = fresh;
2045 }
2046 function exitOverviewToDesktop(mgr, desktopId) {
2047 switchDesktop(mgr, desktopId);
2048 exitOverview(mgr);
2049 }
2050 function createOverviewLabel(item) {
2051 const label = document.createElement("div");
2052 label.className = "desktop-mode-overview-label";
2053 label.dataset.windowId = item.win.id;
2054 const thumbW = item.win.element.offsetWidth * item.scale;
2055 label.style.left = `${item.x}px`;
2056 label.style.top = `${item.y - 34}px`;
2057 label.style.width = `${thumbW}px`;
2058 const iconClass = item.win.config.icon || "dashicons-admin-generic";
2059 const icon = document.createElement("span");
2060 icon.className = `desktop-mode-overview-label__icon dashicons ${iconClass}`;
2061 icon.setAttribute("aria-hidden", "true");
2062 label.appendChild(icon);
2063 const title = document.createElement("span");
2064 title.className = "desktop-mode-overview-label__title";
2065 title.textContent = item.win.config.title;
2066 label.appendChild(title);
2067 const tabCount = item.win.getExternalTabCount();
2068 if (tabCount > 0) {
2069 const meta = document.createElement("span");
2070 meta.className = "desktop-mode-overview-label__meta";
2071 meta.textContent = sprintf(
2072 // translators: %d is the number of external sub-tabs open on this window.
2073 _n("· %d open tab", "· %d open tabs", tabCount),
2074 tabCount
2075 );
2076 label.appendChild(meta);
2077 }
2078 return label;
2079 }
2080 function exitOverview(mgr, selected, maximize = false) {
2081 if (!mgr._overviewActive) {
2082 return;
2083 }
2084 mgr._overviewActive = false;
2085 mgr._overviewAddTileFocused = false;
2086 doAction(HOOKS.OVERVIEW_EXITING, {
2087 windowId: selected ? selected.id : void 0,
2088 reason: selected ? "select" : "cancel"
2089 });
2090 mgr._desktop.classList.remove("desktop-mode-area--overview");
2091 const shell = document.getElementById("desktop-mode-shell");
2092 shell?.classList.remove("desktop-mode-shell--overview");
2093 for (const id of OVERVIEW_INERT_ELEMENTS) {
2094 const el = document.getElementById(id);
2095 if (el) {
2096 el.inert = false;
2097 }
2098 }
2099 inertWpBodyContentChildren(false);
2100 inertWindowChildren(mgr, false);
2101 for (const [id, snap] of mgr._overviewSnapshot) {
2102 const w = mgr.getById(id);
2103 if (!w) {
2104 continue;
2105 }
2106 w.element.style.transform = snap.transform;
2107 }
2108 if (selected) {
2109 mgr.focus(selected);
2110 if (maximize) {
2111 selected.maximize();
2112 }
2113 }
2114 for (const label of mgr._overviewLabels.values()) {
2115 label.classList.add("desktop-mode-overview-label--out");
2116 }
2117 if (mgr._overviewTopBar) {
2118 mgr._overviewTopBar.classList.add(
2119 "desktop-mode-overview-top-bar--out"
2120 );
2121 }
2122 const ANIMATION_MS = 280;
2123 mgr._overviewExitTimeoutId = window.setTimeout(() => {
2124 mgr._overviewExitTimeoutId = null;
2125 for (const w of mgr._stack) {
2126 w.element.classList.remove("desktop-mode-window--overview");
2127 }
2128 for (const label of mgr._overviewLabels.values()) {
2129 label.remove();
2130 }
2131 mgr._overviewLabels.clear();
2132 mgr._overviewSnapshot.clear();
2133 if (mgr._overviewTopBar) {
2134 mgr._overviewTopBar.remove();
2135 mgr._overviewTopBar = null;
2136 }
2137 if (mgr._overviewClickBlocker) {
2138 mgr._desktop.removeEventListener(
2139 "click",
2140 mgr._overviewClickBlocker,
2141 true
2142 );
2143 mgr._overviewClickBlocker = null;
2144 }
2145 doAction(HOOKS.OVERVIEW_EXITED, {
2146 windowId: selected ? selected.id : void 0,
2147 reason: selected ? "select" : "cancel"
2148 });
2149 }, ANIMATION_MS);
2150 if (mgr._overviewPointerDownHandler) {
2151 mgr._desktop.removeEventListener(
2152 "pointerdown",
2153 mgr._overviewPointerDownHandler,
2154 true
2155 );
2156 mgr._overviewPointerDownHandler = null;
2157 }
2158 if (mgr._overviewPointerUpHandler) {
2159 mgr._desktop.removeEventListener(
2160 "pointerup",
2161 mgr._overviewPointerUpHandler,
2162 true
2163 );
2164 mgr._overviewPointerUpHandler = null;
2165 }
2166 mgr._overviewPressTarget = null;
2167 if (mgr._overviewKeyHandler) {
2168 document.removeEventListener("keydown", mgr._overviewKeyHandler);
2169 mgr._overviewKeyHandler = null;
2170 }
2171 if (mgr._overviewMouseHandler) {
2172 mgr._desktop.removeEventListener(
2173 "mouseover",
2174 mgr._overviewMouseHandler
2175 );
2176 mgr._overviewMouseHandler = null;
2177 }
2178 if (mgr._lastOverviewHoverId) {
2179 doAction(HOOKS.OVERVIEW_WINDOW_UNHOVER, {
2180 windowId: mgr._lastOverviewHoverId
2181 });
2182 mgr._lastOverviewHoverId = null;
2183 }
2184 }
2185 function getDesktops(mgr) {
2186 return [...mgr._desktops];
2187 }
2188 function getActiveDesktop(mgr) {
2189 const found = mgr._desktops.find((d) => d.id === mgr._activeDesktopId);
2190 return found ?? mgr._desktops[0];
2191 }
2192 function getActiveDesktopId(mgr) {
2193 return getActiveDesktop(mgr).id;
2194 }
2195 function applyDesktopVisibility(mgr, win) {
2196 const visible = win.config.desktopId === mgr._activeDesktopId;
2197 win.element.style.display = visible ? "" : "none";
2198 }
2199 function refreshDesktopVisibility(mgr) {
2200 for (const w of mgr._stack) {
2201 applyDesktopVisibility(mgr, w);
2202 }
2203 }
2204 function createDesktop(mgr) {
2205 mgr._desktopSeq++;
2206 const desktop = {
2207 id: `desktop-${mgr._desktopSeq}`,
2208 // translators: %d is the desktop number (e.g., "Desktop 2")
2209 label: sprintf(__("Desktop %d"), mgr._desktopSeq)
2210 };
2211 mgr._desktops.push(desktop);
2212 doAction(HOOKS.DESKTOP_CREATED, { desktopId: desktop.id });
2213 return desktop;
2214 }
2215 function switchDesktop(mgr, id, opts) {
2216 if (id === mgr._activeDesktopId) {
2217 return;
2218 }
2219 if (!mgr._desktops.some((d) => d.id === id)) {
2220 return;
2221 }
2222 const previousId = mgr._activeDesktopId;
2223 mgr._activeDesktopId = id;
2224 if (mgr._overviewActive) {
2225 relayoutOverviewForActiveDesktop(mgr);
2226 refreshOverviewTopBar(mgr);
2227 } else {
2228 refreshDesktopVisibility(mgr);
2229 if (opts?.direction) {
2230 animateDesktopSwitch(mgr, opts.direction);
2231 }
2232 const topOnNew = [...mgr._stack].reverse().find(
2233 (w) => w.config.desktopId === id && w.state !== "minimized"
2234 );
2235 if (topOnNew) {
2236 mgr.focus(topOnNew);
2237 }
2238 }
2239 doAction(HOOKS.DESKTOP_SWITCHED, {
2240 from: previousId,
2241 to: id
2242 });
2243 }
2244 function animateDesktopSwitch(mgr, direction) {
2245 const el = mgr._desktop;
2246 const cls = direction === "next" ? "desktop-mode-area--sliding-from-right" : "desktop-mode-area--sliding-from-left";
2247 el.classList.remove(
2248 "desktop-mode-area--sliding-from-right",
2249 "desktop-mode-area--sliding-from-left"
2250 );
2251 void el.offsetWidth;
2252 el.classList.add(cls);
2253 const onEnd = (e) => {
2254 if (!e.animationName.startsWith("desktop-mode-area-slide-from-")) {
2255 return;
2256 }
2257 el.classList.remove(cls);
2258 el.removeEventListener("animationend", onEnd);
2259 };
2260 el.addEventListener("animationend", onEnd);
2261 }
2262 function closeDesktop(mgr, id) {
2263 if (mgr._desktops.length <= 1) {
2264 return;
2265 }
2266 const idx = mgr._desktops.findIndex((d) => d.id === id);
2267 if (idx === -1) {
2268 return;
2269 }
2270 const survivorIdx = idx > 0 ? idx - 1 : 1;
2271 const survivor = mgr._desktops[survivorIdx];
2272 for (const w of mgr._stack) {
2273 if (w.config.desktopId === id) {
2274 w.config.desktopId = survivor.id;
2275 }
2276 }
2277 mgr._desktops.splice(idx, 1);
2278 const wasActive = mgr._activeDesktopId === id;
2279 if (wasActive) {
2280 mgr._activeDesktopId = survivor.id;
2281 }
2282 if (mgr._overviewActive) {
2283 relayoutOverviewForActiveDesktop(mgr);
2284 } else {
2285 refreshDesktopVisibility(mgr);
2286 }
2287 doAction(HOOKS.DESKTOP_CLOSED, {
2288 desktopId: id,
2289 migratedTo: survivor.id
2290 });
2291 }
2292 function relayoutOverviewForActiveDesktop(mgr) {
2293 for (const [winId, snap] of mgr._overviewSnapshot) {
2294 const w = mgr.getById(winId);
2295 if (w) {
2296 w.element.style.transform = snap.transform;
2297 w.element.style.transition = snap.transition;
2298 w.element.classList.remove("desktop-mode-window--overview");
2299 }
2300 }
2301 for (const label of mgr._overviewLabels.values()) {
2302 label.remove();
2303 }
2304 mgr._overviewLabels.clear();
2305 mgr._overviewSnapshot.clear();
2306 refreshDesktopVisibility(mgr);
2307 const eligible = mgr._stack.filter(
2308 (w) => w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
2309 );
2310 if (eligible.length === 0) {
2311 return;
2312 }
2313 for (const w of eligible) {
2314 mgr._overviewSnapshot.set(w.id, {
2315 transform: w.element.style.transform || "",
2316 transition: w.element.style.transition || ""
2317 });
2318 }
2319 const live = mgr._desktop.getBoundingClientRect();
2320 const targetRect = new DOMRect(0, 0, live.width, live.height);
2321 const layout = computeOverviewLayout(
2322 eligible,
2323 targetRect,
2324 OVERVIEW_TOP_BAR_RESERVE
2325 );
2326 for (const item of layout) {
2327 const el = item.win.element;
2328 el.classList.add("desktop-mode-window--overview");
2329 const dx = item.x - el.offsetLeft;
2330 const dy = item.y - el.offsetTop;
2331 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
2332 const label = createOverviewLabel(item);
2333 el.insertAdjacentElement("afterend", label);
2334 mgr._overviewLabels.set(item.win.id, label);
2335 }
2336 }
2337 function seedDesktops(mgr, desktops, activeDesktopId) {
2338 if (desktops.length === 0) {
2339 return;
2340 }
2341 mgr._desktops = desktops.map((d) => ({ ...d }));
2342 mgr._activeDesktopId = desktops.some((d) => d.id === activeDesktopId) ? activeDesktopId : desktops[0].id;
2343 let highest = 0;
2344 for (const d of desktops) {
2345 const match = d.id.match(/^desktop-(\d+)$/);
2346 if (match) {
2347 const n = parseInt(match[1], 10);
2348 if (Number.isFinite(n) && n > highest) {
2349 highest = n;
2350 }
2351 }
2352 }
2353 mgr._desktopSeq = Math.max(mgr._desktopSeq, highest);
2354 }
2355 function cascade(mgr) {
2356 const eligible = mgr._stack.filter(
2357 (w) => w.config.desktopId === mgr._activeDesktopId
2358 );
2359 if (eligible.length === 0) {
2360 return;
2361 }
2362 doAction(HOOKS.ARRANGE_CASCADE_STARTING, {
2363 windowCount: eligible.length
2364 });
2365 for (const w of eligible) {
2366 if (w.state === "minimized") {
2367 w.restore();
2368 }
2369 if (w.state === "fullscreen") {
2370 w.toggleFullscreen();
2371 }
2372 if (w.state === "maximized") {
2373 w.toggleMaximize();
2374 }
2375 }
2376 const rect = mgr._desktop.getBoundingClientRect();
2377 const padding = 30;
2378 const offset = 30;
2379 const targetWidth = Math.min(Math.round(rect.width * 0.7), 1100);
2380 const targetHeight = Math.min(Math.round(rect.height * 0.75), 750);
2381 const maxStepsX = Math.max(
2382 1,
2383 Math.floor((rect.width - targetWidth - padding) / offset)
2384 );
2385 const maxStepsY = Math.max(
2386 1,
2387 Math.floor((rect.height - targetHeight - padding) / offset)
2388 );
2389 const maxSteps = Math.min(maxStepsX, maxStepsY);
2390 eligible.forEach((w, i) => {
2391 const step = i % Math.max(1, maxSteps);
2392 w.element.style.left = `${padding + step * offset}px`;
2393 w.element.style.top = `${padding + step * offset}px`;
2394 w.element.style.width = `${targetWidth}px`;
2395 w.element.style.height = `${targetHeight}px`;
2396 });
2397 const focused = mgr.getFocused();
2398 if (focused) {
2399 mgr.focus(focused);
2400 }
2401 document.dispatchEvent(
2402 new CustomEvent("desktop-mode-window-changed", {
2403 detail: { reason: "cascade" }
2404 })
2405 );
2406 doAction(HOOKS.ARRANGE_CASCADE_APPLIED, {
2407 windowCount: eligible.length
2408 });
2409 }
2410 function tile(mgr) {
2411 const eligible = mgr._stack.filter(
2412 (w) => w.config.desktopId === mgr._activeDesktopId
2413 );
2414 if (eligible.length === 0) {
2415 return;
2416 }
2417 for (const w of eligible) {
2418 if (w.state === "minimized") {
2419 w.restore();
2420 }
2421 if (w.state === "fullscreen") {
2422 w.toggleFullscreen();
2423 }
2424 if (w.state === "maximized") {
2425 w.toggleMaximize();
2426 }
2427 }
2428 const rect = mgr._desktop.getBoundingClientRect();
2429 const auto = pickGridDimensions(
2430 eligible.length,
2431 rect.width,
2432 rect.height
2433 );
2434 const filtered = applyFilters(
2435 HOOKS.ARRANGE_TILE_DIMENSIONS,
2436 auto,
2437 {
2438 windowCount: eligible.length,
2439 areaWidth: rect.width,
2440 areaHeight: rect.height
2441 }
2442 );
2443 const { cols, rows } = isValidGrid(filtered, eligible.length) ? { cols: Math.floor(filtered.cols), rows: Math.floor(filtered.rows) } : auto;
2444 doAction(HOOKS.ARRANGE_TILE_STARTING, {
2445 windowCount: eligible.length,
2446 cols,
2447 rows
2448 });
2449 const padding = 16;
2450 const gap = 12;
2451 const cellWidth = Math.floor(
2452 (rect.width - padding * 2 - gap * (cols - 1)) / cols
2453 );
2454 const cellHeight = Math.floor(
2455 (rect.height - padding * 2 - gap * (rows - 1)) / rows
2456 );
2457 eligible.forEach((w, i) => {
2458 const col = i % cols;
2459 const row = Math.floor(i / cols);
2460 w.element.style.left = `${padding + col * (cellWidth + gap)}px`;
2461 w.element.style.top = `${padding + row * (cellHeight + gap)}px`;
2462 w.element.style.width = `${cellWidth}px`;
2463 w.element.style.height = `${cellHeight}px`;
2464 });
2465 const focused = mgr.getFocused();
2466 if (focused) {
2467 mgr.focus(focused);
2468 }
2469 document.dispatchEvent(
2470 new CustomEvent("desktop-mode-window-changed", {
2471 detail: { reason: "tile" }
2472 })
2473 );
2474 doAction(HOOKS.ARRANGE_TILE_APPLIED, {
2475 windowCount: eligible.length,
2476 cols,
2477 rows
2478 });
2479 }
2480 const SNAP_STORAGE_KEY = "desktop-mode-snap-to-grid";
2481 function loadSnapEnabled() {
2482 try {
2483 return window.localStorage.getItem(SNAP_STORAGE_KEY) === "1";
2484 } catch {
2485 return false;
2486 }
2487 }
2488 function setSnapEnabled(mgr, enabled) {
2489 if (mgr._snapEnabled === enabled) {
2490 return;
2491 }
2492 mgr._snapEnabled = enabled;
2493 try {
2494 window.localStorage.setItem(SNAP_STORAGE_KEY, enabled ? "1" : "0");
2495 } catch {
2496 }
2497 doAction(HOOKS.ARRANGE_SNAP_CHANGED, { enabled });
2498 }
2499 function getSnapConfig(mgr) {
2500 if (!mgr._snapEnabled) {
2501 return { enabled: false, cellWidth: 0, cellHeight: 0 };
2502 }
2503 const rect = mgr._desktop.getBoundingClientRect();
2504 const targetCols = rect.width >= rect.height ? 12 : 8;
2505 const auto = {
2506 cellWidth: Math.max(40, Math.round(rect.width / targetCols)),
2507 cellHeight: Math.max(
2508 40,
2509 Math.round(rect.height / Math.round(targetCols * 0.66))
2510 )
2511 };
2512 const filtered = applyFilters(
2513 HOOKS.ARRANGE_SNAP_CELL_SIZE,
2514 auto,
2515 { areaWidth: rect.width, areaHeight: rect.height }
2516 );
2517 const { cellWidth, cellHeight } = isValidCellSize(filtered) ? filtered : auto;
2518 return { enabled: true, cellWidth, cellHeight };
2519 }
2520 function enterSplitOverview(mgr, anchor, zone) {
2521 if (mgr._splitOverviewActive) {
2522 return;
2523 }
2524 mgr._splitOverviewActive = true;
2525 mgr._splitOverviewAnchor = anchor;
2526 mgr._splitOverviewZone = zone;
2527 const eligible = mgr._stack.filter(
2528 (w) => w !== anchor && w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
2529 );
2530 if (eligible.length === 0) {
2531 cleanupSplitOverviewState(mgr);
2532 return;
2533 }
2534 mgr._splitOverviewSnapshot.clear();
2535 for (const w of eligible) {
2536 mgr._splitOverviewSnapshot.set(w.id, {
2537 transform: w.element.style.transform || "",
2538 transition: w.element.style.transition || ""
2539 });
2540 }
2541 mgr._desktop.classList.add("desktop-mode-area--split-overview");
2542 const rect = oppositeHalfRect(mgr, zone);
2543 const layout = computeOverviewLayout(eligible, rect, 0);
2544 mgr._splitOverviewLabels.clear();
2545 for (const item of layout) {
2546 const el = item.win.element;
2547 el.classList.add("desktop-mode-window--overview");
2548 const dx = item.x - el.offsetLeft;
2549 const dy = item.y - el.offsetTop;
2550 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
2551 const label = createOverviewLabel(item);
2552 el.insertAdjacentElement("afterend", label);
2553 mgr._splitOverviewLabels.set(item.win.id, label);
2554 }
2555 const pressTargetForEvent = (e) => {
2556 const target2 = e.target;
2557 const winEl = target2?.closest(
2558 ".desktop-mode-window--overview"
2559 );
2560 if (winEl) {
2561 return {
2562 id: winEl.id.replace(/^wp-window-/, ""),
2563 element: winEl
2564 };
2565 }
2566 if (target2) {
2567 return { id: "dismiss", element: mgr._desktop };
2568 }
2569 return null;
2570 };
2571 mgr._splitOverviewPointerDown = (e) => {
2572 if (e.button !== 0) {
2573 mgr._splitOverviewPressTarget = null;
2574 return;
2575 }
2576 mgr._splitOverviewPressTarget = pressTargetForEvent(e);
2577 if (mgr._splitOverviewPressTarget) {
2578 e.preventDefault();
2579 e.stopPropagation();
2580 }
2581 };
2582 mgr._splitOverviewPointerUp = (e) => {
2583 if (e.button !== 0) {
2584 return;
2585 }
2586 const pressed = mgr._splitOverviewPressTarget;
2587 mgr._splitOverviewPressTarget = null;
2588 if (!pressed) {
2589 return;
2590 }
2591 const r = pressed.element.getBoundingClientRect();
2592 const inside = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
2593 if (!inside) {
2594 return;
2595 }
2596 e.preventDefault();
2597 e.stopPropagation();
2598 if (pressed.id === "dismiss") {
2599 exitSplitOverview(mgr);
2600 return;
2601 }
2602 const selected = mgr.getById(pressed.id);
2603 if (!selected) {
2604 exitSplitOverview(mgr);
2605 return;
2606 }
2607 fillOppositeHalfAndExit(mgr, selected);
2608 };
2609 mgr._splitOverviewKey = (e) => {
2610 if (e.key === "Escape") {
2611 exitSplitOverview(mgr);
2612 }
2613 };
2614 mgr._splitOverviewClickBlocker = (e) => {
2615 e.stopPropagation();
2616 e.preventDefault();
2617 };
2618 mgr._desktop.addEventListener(
2619 "pointerdown",
2620 mgr._splitOverviewPointerDown,
2621 true
2622 );
2623 mgr._desktop.addEventListener(
2624 "pointerup",
2625 mgr._splitOverviewPointerUp,
2626 true
2627 );
2628 mgr._desktop.addEventListener(
2629 "click",
2630 mgr._splitOverviewClickBlocker,
2631 true
2632 );
2633 document.addEventListener("keydown", mgr._splitOverviewKey);
2634 }
2635 function fillOppositeHalfAndExit(mgr, selected) {
2636 const anchorZone = mgr._splitOverviewZone;
2637 if (!anchorZone) {
2638 exitSplitOverview(mgr);
2639 return;
2640 }
2641 const partnerZone = anchorZone === "left" ? "right" : "left";
2642 selected.element.style.transform = "";
2643 selected.element.classList.remove("desktop-mode-window--overview");
2644 selected.applySnap(partnerZone);
2645 mgr._splitOverviewSnapshot.delete(selected.id);
2646 mgr.focus(selected);
2647 doAction(HOOKS.SNAP_SPLIT_FILLED, {
2648 windowId: selected.id,
2649 zone: partnerZone
2650 });
2651 exitSplitOverview(mgr);
2652 }
2653 function exitSplitOverview(mgr) {
2654 if (!mgr._splitOverviewActive) {
2655 return;
2656 }
2657 mgr._splitOverviewActive = false;
2658 for (const [id, snap] of mgr._splitOverviewSnapshot) {
2659 const w = mgr.getById(id);
2660 if (!w) {
2661 continue;
2662 }
2663 w.element.style.transform = snap.transform;
2664 }
2665 for (const label of mgr._splitOverviewLabels.values()) {
2666 label.classList.add("desktop-mode-overview-label--out");
2667 }
2668 mgr._desktop.classList.remove("desktop-mode-area--split-overview");
2669 const ANIMATION_MS = 260;
2670 window.setTimeout(() => {
2671 for (const w of mgr._stack) {
2672 if (mgr._splitOverviewSnapshot.has(w.id)) {
2673 w.element.classList.remove("desktop-mode-window--overview");
2674 }
2675 }
2676 for (const label of mgr._splitOverviewLabels.values()) {
2677 label.remove();
2678 }
2679 cleanupSplitOverviewState(mgr);
2680 }, ANIMATION_MS);
2681 if (mgr._splitOverviewPointerDown) {
2682 mgr._desktop.removeEventListener(
2683 "pointerdown",
2684 mgr._splitOverviewPointerDown,
2685 true
2686 );
2687 mgr._splitOverviewPointerDown = null;
2688 }
2689 if (mgr._splitOverviewPointerUp) {
2690 mgr._desktop.removeEventListener(
2691 "pointerup",
2692 mgr._splitOverviewPointerUp,
2693 true
2694 );
2695 mgr._splitOverviewPointerUp = null;
2696 }
2697 if (mgr._splitOverviewClickBlocker) {
2698 mgr._desktop.removeEventListener(
2699 "click",
2700 mgr._splitOverviewClickBlocker,
2701 true
2702 );
2703 mgr._splitOverviewClickBlocker = null;
2704 }
2705 if (mgr._splitOverviewKey) {
2706 document.removeEventListener("keydown", mgr._splitOverviewKey);
2707 mgr._splitOverviewKey = null;
2708 }
2709 mgr._splitOverviewPressTarget = null;
2710 }
2711 function cleanupSplitOverviewState(mgr) {
2712 mgr._splitOverviewSnapshot.clear();
2713 mgr._splitOverviewLabels.clear();
2714 mgr._splitOverviewAnchor = null;
2715 mgr._splitOverviewZone = null;
2716 mgr._splitOverviewActive = false;
2717 }
2718 const SNAP_EDGE_THRESHOLD = 30;
2719 const SNAP_COMMIT_MS = 260;
2720 function detectSnapZone(clientX, desktopRect) {
2721 if (clientX <= desktopRect.left + SNAP_EDGE_THRESHOLD) {
2722 return "left";
2723 }
2724 if (clientX >= desktopRect.right - SNAP_EDGE_THRESHOLD) {
2725 return "right";
2726 }
2727 return null;
2728 }
2729 function snapZoneBounds(mgr, zone) {
2730 const rect = mgr._desktop.getBoundingClientRect();
2731 const halfW = Math.floor(rect.width / 2);
2732 const height = Math.floor(rect.height);
2733 return {
2734 x: zone === "left" ? 0 : rect.width - halfW,
2735 y: 0,
2736 width: halfW,
2737 height
2738 };
2739 }
2740 function oppositeHalfRect(mgr, zone) {
2741 const rect = mgr._desktop.getBoundingClientRect();
2742 const halfW = Math.floor(rect.width / 2);
2743 const height = Math.floor(rect.height);
2744 if (zone === "left") {
2745 return new DOMRect(halfW, 0, halfW, height);
2746 }
2747 return new DOMRect(0, 0, halfW, height);
2748 }
2749 function showSnapPreview(mgr, zone) {
2750 if (mgr._snapPendingZone === zone && mgr._snapPreviewEl) {
2751 return;
2752 }
2753 mgr._snapPendingZone = zone;
2754 if (!mgr._snapPreviewEl) {
2755 const el = document.createElement("div");
2756 el.className = "desktop-mode-snap-preview";
2757 el.setAttribute("aria-hidden", "true");
2758 mgr._desktop.appendChild(el);
2759 mgr._snapPreviewEl = el;
2760 Promise.resolve().then(() => {
2761 el.classList.add("desktop-mode-snap-preview--visible");
2762 });
2763 }
2764 const b = snapZoneBounds(mgr, zone);
2765 mgr._snapPreviewEl.style.left = `${b.x}px`;
2766 mgr._snapPreviewEl.style.top = `${b.y}px`;
2767 mgr._snapPreviewEl.style.width = `${b.width}px`;
2768 mgr._snapPreviewEl.style.height = `${b.height}px`;
2769 mgr._snapPreviewEl.dataset.zone = zone;
2770 }
2771 function hideSnapPreview(mgr) {
2772 if (!mgr._snapPreviewEl) {
2773 mgr._snapPendingZone = null;
2774 return;
2775 }
2776 const el = mgr._snapPreviewEl;
2777 mgr._snapPreviewEl = null;
2778 mgr._snapPendingZone = null;
2779 el.classList.remove("desktop-mode-snap-preview--visible");
2780 window.setTimeout(() => {
2781 el.remove();
2782 }, SNAP_COMMIT_MS);
2783 }
2784 function updateSnapZoneForDrag(mgr, win, clientX) {
2785 if (mgr._splitOverviewActive) {
2786 return;
2787 }
2788 const rect = mgr._desktop.getBoundingClientRect();
2789 const zone = detectSnapZone(clientX, rect);
2790 const previous = mgr._snapPendingZone;
2791 if (zone) {
2792 showSnapPreview(mgr, zone);
2793 if (previous !== zone) {
2794 doAction(HOOKS.SNAP_ZONE_PENDING, {
2795 windowId: win.id,
2796 zone
2797 });
2798 }
2799 } else if (previous) {
2800 hideSnapPreview(mgr);
2801 doAction(HOOKS.SNAP_ZONE_CANCELED, { windowId: win.id });
2802 }
2803 }
2804 function commitSnapIfPending(mgr, win) {
2805 const zone = mgr._snapPendingZone;
2806 if (!zone) {
2807 return false;
2808 }
2809 hideSnapPreview(mgr);
2810 if (win.state === "normal") {
2811 win._savedGeometry = {
2812 x: win.element.offsetLeft,
2813 y: win.element.offsetTop,
2814 width: win.element.offsetWidth,
2815 height: win.element.offsetHeight
2816 };
2817 }
2818 win.applySnap(zone);
2819 doAction(HOOKS.SNAP_ZONE_COMMITTED, {
2820 windowId: win.id,
2821 zone
2822 });
2823 window.requestAnimationFrame(() => {
2824 enterSplitOverview(mgr, win, zone);
2825 });
2826 return true;
2827 }
2828 function abortSnapIfPending(mgr) {
2829 if (mgr._snapPendingZone) {
2830 hideSnapPreview(mgr);
2831 }
2832 }
2833 const NATIVE_GEOMETRY_STORAGE_KEY = "desktop-mode-native-window-geometry";
2834 const MAX_ENTRIES = 64;
2835 const MAX_DIMENSION = 8192;
2836 function readMap$1() {
2837 try {
2838 const raw = window.localStorage.getItem(NATIVE_GEOMETRY_STORAGE_KEY);
2839 if (!raw) {
2840 return {};
2841 }
2842 const parsed = JSON.parse(raw);
2843 if (!parsed || typeof parsed !== "object") {
2844 return {};
2845 }
2846 return parsed;
2847 } catch {
2848 return {};
2849 }
2850 }
2851 function writeMap$1(map) {
2852 try {
2853 window.localStorage.setItem(
2854 NATIVE_GEOMETRY_STORAGE_KEY,
2855 JSON.stringify(map)
2856 );
2857 } catch {
2858 }
2859 }
2860 function loadNativeWindowGeometry(baseId) {
2861 if (!baseId) {
2862 return null;
2863 }
2864 const map = readMap$1();
2865 const entry = map[baseId];
2866 if (!entry) {
2867 return null;
2868 }
2869 const width = Number(entry.width);
2870 const height = Number(entry.height);
2871 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2872 return null;
2873 }
2874 const state2 = entry.state === "maximized" ? "maximized" : void 0;
2875 const x = Number(entry.x);
2876 const y = Number(entry.y);
2877 const hasPosition = Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0 && x <= MAX_DIMENSION && y <= MAX_DIMENSION;
2878 return {
2879 width: Math.round(width),
2880 height: Math.round(height),
2881 ...hasPosition ? { x: Math.round(x), y: Math.round(y) } : {},
2882 ...state2 ? { state: state2 } : {}
2883 };
2884 }
2885 function saveNativeWindowGeometry(baseId, geometry) {
2886 if (!baseId) {
2887 return;
2888 }
2889 const width = Math.round(Number(geometry.width));
2890 const height = Math.round(Number(geometry.height));
2891 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2892 return;
2893 }
2894 const map = readMap$1();
2895 const prev = map[baseId];
2896 const state2 = prev && prev.state === "maximized" ? "maximized" : void 0;
2897 const carriedX = typeof prev?.x === "number" ? prev.x : void 0;
2898 const carriedY = typeof prev?.y === "number" ? prev.y : void 0;
2899 if (prev && prev.width === width && prev.height === height && prev.state === state2 && prev.x === carriedX && prev.y === carriedY) {
2900 return;
2901 }
2902 upsertEntry(map, baseId, {
2903 width,
2904 height,
2905 ...typeof carriedX === "number" && typeof carriedY === "number" ? { x: carriedX, y: carriedY } : {},
2906 ...state2 ? { state: state2 } : {}
2907 });
2908 writeMapTrimmed(map);
2909 }
2910 function saveNativeWindowPosition(baseId, position) {
2911 if (!baseId) {
2912 return;
2913 }
2914 const x = Math.round(Number(position.x));
2915 const y = Math.round(Number(position.y));
2916 if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || y < 0 || x > MAX_DIMENSION || y > MAX_DIMENSION) {
2917 return;
2918 }
2919 const map = readMap$1();
2920 const prev = map[baseId];
2921 if (!prev) {
2922 return;
2923 }
2924 if (prev.x === x && prev.y === y) {
2925 return;
2926 }
2927 upsertEntry(map, baseId, {
2928 ...prev,
2929 x,
2930 y
2931 });
2932 writeMapTrimmed(map);
2933 }
2934 function setNativeWindowSavedState(baseId, state2, defaults) {
2935 if (!baseId) {
2936 return;
2937 }
2938 const map = readMap$1();
2939 const prev = map[baseId];
2940 if (!prev) {
2941 if (state2 === null || !defaults) {
2942 return;
2943 }
2944 const width = Math.round(Number(defaults.width));
2945 const height = Math.round(Number(defaults.height));
2946 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2947 return;
2948 }
2949 upsertEntry(map, baseId, { width, height, state: state2 });
2950 writeMapTrimmed(map);
2951 return;
2952 }
2953 if (state2 === null) {
2954 if (!prev.state) {
2955 return;
2956 }
2957 const { state: _state2, ...rest } = prev;
2958 upsertEntry(map, baseId, rest);
2959 writeMapTrimmed(map);
2960 return;
2961 }
2962 if (prev.state === state2) {
2963 return;
2964 }
2965 upsertEntry(map, baseId, {
2966 ...prev,
2967 state: state2
2968 });
2969 writeMapTrimmed(map);
2970 }
2971 function upsertEntry(map, baseId, entry) {
2972 delete map[baseId];
2973 map[baseId] = entry;
2974 }
2975 function writeMapTrimmed(map) {
2976 const keys = Object.keys(map);
2977 if (keys.length > MAX_ENTRIES) {
2978 const trimmed = {};
2979 for (const key of keys.slice(-MAX_ENTRIES)) {
2980 trimmed[key] = map[key];
2981 }
2982 writeMap$1(trimmed);
2983 return;
2984 }
2985 writeMap$1(map);
2986 }
2987 const BASE_Z_INDEX = 100;
2988 const CASCADE_OFFSET = 30;
2989 class WindowManager {
2990 constructor(desktop) {
2991 this._stack = [];
2992 this.cascadeIndex = 0;
2993 this._desktops = [
2994 // translators: default desktop name — "Desktop 1"
2995 { id: "desktop-1", label: "Desktop 1" }
2996 ];
2997 this._activeDesktopId = "desktop-1";
2998 this._desktopSeq = 1;
2999 this.onToggleStartupRequested = null;
3000 this.desktopResizeObserver = null;
3001 this._reflowRestoreTimer = null;
3002 this._snapEnabled = loadSnapEnabled();
3003 this._overviewActive = false;
3004 this._overviewSnapshot = /* @__PURE__ */ new Map();
3005 this._overviewLabels = /* @__PURE__ */ new Map();
3006 this._overviewPointerDownHandler = null;
3007 this._overviewPointerUpHandler = null;
3008 this._overviewKeyHandler = null;
3009 this._overviewPressTarget = null;
3010 this._overviewClickBlocker = null;
3011 this._overviewTopBar = null;
3012 this._overviewMouseHandler = null;
3013 this._lastOverviewHoverId = null;
3014 this._overviewAddTileFocused = false;
3015 this._overviewEnterTimeoutId = null;
3016 this._overviewExitTimeoutId = null;
3017 this._snapPendingZone = null;
3018 this._snapPreviewEl = null;
3019 this._splitOverviewActive = false;
3020 this._splitOverviewAnchor = null;
3021 this._splitOverviewZone = null;
3022 this._splitOverviewSnapshot = /* @__PURE__ */ new Map();
3023 this._splitOverviewLabels = /* @__PURE__ */ new Map();
3024 this._splitOverviewPointerDown = null;
3025 this._splitOverviewPointerUp = null;
3026 this._splitOverviewPressTarget = null;
3027 this._splitOverviewClickBlocker = null;
3028 this._splitOverviewKey = null;
3029 this._desktop = desktop;
3030 if (typeof ResizeObserver !== "undefined") {
3031 this.desktopResizeObserver = new ResizeObserver(
3032 () => this.reflowStatefulWindows()
3033 );
3034 this.desktopResizeObserver.observe(desktop);
3035 }
3036 this.installIframeFocusBridge();
3037 }
3038 /**
3039 * Clicks inside an iframe don't cross the browsing-context
3040 * boundary — pointerdown / focusin in the iframe's document never
3041 * reach the parent. BUT the parent `window` does lose focus,
3042 * because focus moves to the iframe's content window.
3043 *
3044 * We use that signal: listen for `window.blur` on the parent,
3045 * check `document.activeElement` — if it's an iframe, walk up to
3046 * its owning `.desktop-mode-window`, find the matching Window in
3047 * our stack, and focus it. Covers clicks on the primary iframe
3048 * AND any external-tab sub-iframes mounted as descendants of the
3049 * window element.
3050 */
3051 installIframeFocusBridge() {
3052 window.addEventListener("blur", () => {
3053 window.setTimeout(() => {
3054 const active2 = this._desktop.ownerDocument?.activeElement ?? null;
3055 if (!active2 || active2.tagName !== "IFRAME") {
3056 return;
3057 }
3058 const winEl = active2.closest(
3059 ".desktop-mode-window"
3060 );
3061 if (!winEl) {
3062 return;
3063 }
3064 const id = winEl.id.replace(/^wp-window-/, "");
3065 const win = this.getById(id);
3066 if (!win) {
3067 return;
3068 }
3069 if (this._overviewActive) {
3070 return;
3071 }
3072 if (this.getFocused() === win) {
3073 return;
3074 }
3075 this.focus(win);
3076 }, 0);
3077 });
3078 }
3079 /**
3080 * Re-apply state-driven bounds to any window whose geometry is
3081 * derived from the desktop area's dimensions: maximized (full
3082 * area) and snapped-left / snapped-right (half area). Called from
3083 * the desktop-area ResizeObserver so shrinking the browser window
3084 * drags the stateful windows along with it.
3085 *
3086 * Inlines the geometry writes instead of calling `applySnap` —
3087 * that method emits `_emitChange('state')` which would spam the
3088 * session saver on every resize tick. Viewport resize is an
3089 * INCOMING shape change (the shell reshaped us), not an outgoing
3090 * user action worth persisting.
3091 *
3092 * Also toggles `desktop-mode-window--reflowing` so the base
3093 * left/top/width/height transition doesn't interpolate between
3094 * every ResizeObserver tick — without that, the windows would
3095 * always lag ~250 ms behind a browser edge-drag.
3096 *
3097 * Skipped while overview is active — windows are mid-transform
3098 * and touching their inline geometry would desync the live
3099 * transform math; overview exit re-applies state correctly via
3100 * its own path.
3101 */
3102 reflowStatefulWindows() {
3103 if (this._overviewActive) {
3104 return;
3105 }
3106 for (const w of this._stack) {
3107 const parent = w.element.parentElement;
3108 if (!parent) {
3109 continue;
3110 }
3111 if (w.state === "maximized") {
3112 w.element.classList.add("desktop-mode-window--reflowing");
3113 w.element.style.width = `${parent.clientWidth}px`;
3114 w.element.style.height = `${parent.clientHeight}px`;
3115 } else if (w.state === "snapped-left" || w.state === "snapped-right") {
3116 w.element.classList.add("desktop-mode-window--reflowing");
3117 const halfW = Math.floor(parent.clientWidth / 2);
3118 const height = parent.clientHeight;
3119 const left = w.state === "snapped-left" ? 0 : halfW;
3120 w.element.style.left = `${left}px`;
3121 w.element.style.top = "0px";
3122 w.element.style.width = `${halfW}px`;
3123 w.element.style.height = `${height}px`;
3124 }
3125 }
3126 if (this._reflowRestoreTimer !== null) {
3127 window.clearTimeout(this._reflowRestoreTimer);
3128 }
3129 this._reflowRestoreTimer = window.setTimeout(() => {
3130 this._reflowRestoreTimer = null;
3131 for (const w of this._stack) {
3132 w.element.classList.remove("desktop-mode-window--reflowing");
3133 }
3134 }, 140);
3135 }
3136 /**
3137 * Open a new window — or focus an existing one — for the given
3138 * page.
3139 *
3140 * Matches any existing window sharing the same `baseId`
3141 * (defaulting to the config's `id`). For singleton pages
3142 * (Settings, Dashboard, …) `baseId === id`, so this behaves
3143 * exactly like strict id matching. For multi pages, clicking the
3144 * dock icon while a window is already open focuses the
3145 * most-recent instance rather than creating a twin.
3146 *
3147 * URL-aware reuse: when the matched window is NOT already showing
3148 * the requested URL (and the request isn't for the window's home
3149 * / dock landing URL), the existing iframe navigates to it in
3150 * place — an action URL like
3151 * `plugins.php?action=activate&…&_wpnonce=…` actually runs
3152 * instead of being dropped by a bare focus. The
3153 * `desktop-mode-window-reopened` event reports which path was
3154 * taken via its `navigated` flag.
3155 *
3156 * To force a brand-new instance alongside an existing one, use
3157 * {@link openNew}.
3158 */
3159 async open(config) {
3160 if (!config || typeof config !== "object") {
3161 throw new TypeError(
3162 "windowManager.open() requires a config object with at least { id, url, title }; received " + (config === null ? "null" : typeof config)
3163 );
3164 }
3165 if (typeof config.id !== "string" || config.id === "") {
3166 throw new TypeError(
3167 "windowManager.open(): config.id must be a non-empty string."
3168 );
3169 }
3170 if (typeof config.url !== "string" || config.url === "") {
3171 throw new TypeError(
3172 '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.'
3173 );
3174 }
3175 if (typeof config.title !== "string") {
3176 throw new TypeError(
3177 "windowManager.open(): config.title must be a string."
3178 );
3179 }
3180 const baseId = config.baseId || config.id;
3181 const existing = this.getByBaseIdOnActiveDesktop(baseId);
3182 if (existing) {
3183 const wasMinimized = existing.state === "minimized";
3184 this.focus(existing);
3185 if (wasMinimized) {
3186 existing.restore();
3187 }
3188 let navigated = false;
3189 if (!existing.config.native) {
3190 const requestedKey = urlReuseKey(config.url);
3191 const alreadyThere = requestedKey === urlReuseKey(existing.getCurrentUrl()) || requestedKey === urlReuseKey(existing.config.url || "") || requestedKey === urlReuseKey(
3192 existing.config.parentUrl ?? existing.config.url ?? ""
3193 );
3194 if (!alreadyThere) {
3195 navigated = existing.navigateTo(config.url);
3196 }
3197 }
3198 const reopenedDetail = {
3199 windowId: existing.id,
3200 baseId,
3201 wasMinimized,
3202 navigated
3203 };
3204 document.dispatchEvent(
3205 new CustomEvent("desktop-mode-window-reopened", { detail: reopenedDetail })
3206 );
3207 doAction(HOOKS.WINDOW_REOPENED, reopenedDetail);
3208 return existing;
3209 }
3210 const id = this.getByBaseId(baseId) ? this.nextInstanceId(baseId) : config.id;
3211 return this.createWindow({ ...config, id, baseId });
3212 }
3213 /**
3214 * Open a brand-new window even if one is already open for this
3215 * page. Only makes sense for pages flagged `multi`.
3216 *
3217 * Duplicates always open in the floating ('normal') state and at
3218 * a fresh cascade slot — the per-baseId saved size / state /
3219 * position preferences apply to the primary instance only.
3220 * Spawning a maximized twin alongside the maximized primary
3221 * would hide the primary; landing a twin on top of the primary's
3222 * remembered position would hide it too. Callers can override
3223 * either default by passing `initialState` / `x` / `y` explicitly.
3224 */
3225 async openNew(config) {
3226 const baseId = config.baseId || config.id;
3227 const nextId2 = this.nextInstanceId(baseId);
3228 const cascadeX = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
3229 const cascadeY = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
3230 return this.createWindow({
3231 initialState: "normal",
3232 x: cascadeX,
3233 y: cascadeY,
3234 ...config,
3235 id: nextId2,
3236 baseId
3237 });
3238 }
3239 /**
3240 * Build and mount a window element. Common tail shared by
3241 * `open()` and `openNew()`.
3242 */
3243 async createWindow(config) {
3244 const desktopRect = this._desktop.getBoundingClientRect();
3245 const defaultWidth = Math.min(Math.round(desktopRect.width * 0.8), 1200);
3246 const defaultHeight = Math.min(Math.round(desktopRect.height * 0.8), 800);
3247 const cascadeX = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
3248 const cascadeY = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
3249 const resolvedBaseId = config.baseId || config.id;
3250 const minWidth = config.minWidth ?? 320;
3251 const minHeight = config.minHeight ?? 200;
3252 const hasExplicitWidth = typeof config.width === "number";
3253 const hasExplicitHeight = typeof config.height === "number";
3254 const hasExplicitX = typeof config.x === "number";
3255 const hasExplicitY = typeof config.y === "number";
3256 const hasExplicitState = typeof config.initialState === "string";
3257 const saved = !hasExplicitWidth || !hasExplicitHeight || !hasExplicitState || !hasExplicitX || !hasExplicitY ? loadNativeWindowGeometry(resolvedBaseId) : null;
3258 const resolvedWidth = config.width ?? (saved ? Math.max(saved.width, minWidth) : defaultWidth);
3259 const resolvedHeight = config.height ?? (saved ? Math.max(saved.height, minHeight) : defaultHeight);
3260 const resolvedState = config.initialState ?? (saved?.state === "maximized" ? "maximized" : void 0);
3261 let clampedSavedX;
3262 let clampedSavedY;
3263 if (saved && typeof saved.x === "number" && typeof saved.y === "number") {
3264 const margin = 12;
3265 const maxX = Math.max(
3266 0,
3267 desktopRect.width - resolvedWidth - margin
3268 );
3269 const maxY = Math.max(
3270 0,
3271 desktopRect.height - resolvedHeight - margin
3272 );
3273 clampedSavedX = Math.max(margin, Math.min(saved.x, maxX));
3274 clampedSavedY = Math.max(margin, Math.min(saved.y, maxY));
3275 }
3276 const resolvedX = config.x ?? clampedSavedX ?? cascadeX;
3277 const resolvedY = config.y ?? clampedSavedY ?? cascadeY;
3278 const callerPinned = hasExplicitWidth || hasExplicitHeight || hasExplicitX || hasExplicitY || hasExplicitState;
3279 const hasSavedGeometry = !!saved;
3280 const preFilterGeometry = {
3281 x: resolvedX,
3282 y: resolvedY,
3283 width: resolvedWidth,
3284 height: resolvedHeight,
3285 state: resolvedState
3286 };
3287 let filtered;
3288 try {
3289 filtered = applyFilters(
3290 HOOKS.WINDOW_GEOMETRY,
3291 preFilterGeometry,
3292 {
3293 windowId: config.id,
3294 baseId: resolvedBaseId,
3295 hasSavedGeometry,
3296 callerPinned,
3297 desktopRect: {
3298 width: desktopRect.width,
3299 height: desktopRect.height
3300 }
3301 }
3302 );
3303 } catch (err) {
3304 doAction(HOOKS.SHELL_ERROR, {
3305 scope: "window-geometry-filter",
3306 windowId: config.id,
3307 error: err
3308 });
3309 if (typeof console !== "undefined") {
3310 console.error(
3311 `[desktop-mode] WINDOW_GEOMETRY filter threw for "${config.id}":`,
3312 err
3313 );
3314 }
3315 filtered = preFilterGeometry;
3316 }
3317 const coalesce = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : fallback;
3318 const safeFiltered = filtered && typeof filtered === "object" ? filtered : preFilterGeometry;
3319 const finalWidth = Math.max(
3320 coalesce(safeFiltered.width, resolvedWidth),
3321 minWidth
3322 );
3323 const finalHeight = Math.max(
3324 coalesce(safeFiltered.height, resolvedHeight),
3325 minHeight
3326 );
3327 const finalX = coalesce(safeFiltered.x, resolvedX);
3328 const finalY = coalesce(safeFiltered.y, resolvedY);
3329 const finalState = safeFiltered.state ?? resolvedState;
3330 const fullConfig = {
3331 icon: config.icon || "dashicons-admin-generic",
3332 ...config,
3333 // Spread `config` first so callers can pass through any
3334 // extras (render, ownerHandle, parentUrl, …), then pin the
3335 // dimensions + state we resolved above. The pin has to
3336 // follow the spread because an explicit `width: undefined`
3337 // from the caller would otherwise blow away the default.
3338 x: finalX,
3339 y: finalY,
3340 width: finalWidth,
3341 height: finalHeight,
3342 minWidth,
3343 minHeight,
3344 ...finalState ? { initialState: finalState } : {},
3345 baseId: resolvedBaseId,
3346 // New windows always join the active desktop. A caller can
3347 // pre-seed `desktopId` (e.g. session restore) by passing it
3348 // in `config`, which the spread above preserves.
3349 desktopId: config.desktopId || this._activeDesktopId
3350 };
3351 this.cascadeIndex++;
3352 const [system] = await Promise.all([
3353 ensureWindowSystemLoaded(windowSystemBundleUrl()),
3354 ensureShellOverlaysLoaded(shellOverlaysBundleUrl())
3355 ]);
3356 const win = system.createWindow(fullConfig);
3357 win.onFocusRequest = (w) => this.focus(w);
3358 win.onClose = (w) => this.remove(w);
3359 win.onMinimize = () => {
3360 const visible = this._stack.filter((w) => w.state !== "minimized");
3361 if (visible.length > 0) {
3362 this.focus(visible[visible.length - 1]);
3363 }
3364 };
3365 win.onOpenAnother = (w) => {
3366 const baseId = w.config.baseId || w.id;
3367 if (w.config.native) {
3368 const api = window.wp?.desktop;
3369 if (api?.openNewWindow?.(baseId, { source: "open-another" })) {
3370 return;
3371 }
3372 }
3373 void this.openNew({
3374 id: baseId,
3375 baseId,
3376 url: w.config.url || "",
3377 title: w.config.title,
3378 icon: w.config.icon,
3379 submenu: w.config.submenu,
3380 multi: true
3381 });
3382 };
3383 win.onOpenInNewWindow = (w) => {
3384 const baseId = w.config.baseId || w.id;
3385 if (w.config.native) {
3386 const api = window.wp?.desktop;
3387 if (api?.openNewWindow?.(baseId, { source: "open-in-new-window" })) {
3388 return;
3389 }
3390 }
3391 const currentUrl = w.getCurrentUrl();
3392 void this.openNew({
3393 id: baseId,
3394 baseId,
3395 url: currentUrl || w.config.url || "",
3396 title: w.config.title,
3397 icon: w.config.icon,
3398 submenu: w.config.submenu,
3399 multi: true
3400 });
3401 };
3402 win.onToggleStartup = (w) => {
3403 this.onToggleStartupRequested?.(w);
3404 };
3405 win.snapConfigProvider = () => this.getSnapConfig();
3406 win.onDragMove = (w, clientX) => {
3407 updateSnapZoneForDrag(this, w, clientX);
3408 };
3409 win.onDragEnd = (w) => {
3410 if (this._snapPendingZone) {
3411 return commitSnapIfPending(this, w);
3412 }
3413 abortSnapIfPending(this);
3414 return false;
3415 };
3416 this._stack.push(win);
3417 this._desktop.appendChild(win.element);
3418 applyDesktopVisibility(this, win);
3419 win.hydrateNative();
3420 this.focus(win);
3421 const openedDetail = {
3422 windowId: win.id,
3423 page: config.url,
3424 title: config.title,
3425 url: config.url
3426 };
3427 document.dispatchEvent(
3428 new CustomEvent("desktop-mode-window-opened", { detail: openedDetail })
3429 );
3430 doAction(HOOKS.WINDOW_OPENED, openedDetail);
3431 return win;
3432 }
3433 /**
3434 * Find the next unused suffixed id for a given baseId. Prefers
3435 * the bare baseId itself if free (user closed the original), then
3436 * walks `-2`, `-3`, … until it lands on one not currently in the
3437 * stack.
3438 */
3439 nextInstanceId(baseId) {
3440 const taken = new Set(this._stack.map((w) => w.id));
3441 if (!taken.has(baseId)) {
3442 return baseId;
3443 }
3444 let n = 2;
3445 while (taken.has(`${baseId}-${n}`)) {
3446 n++;
3447 }
3448 return `${baseId}-${n}`;
3449 }
3450 /** Focus a window: bring it to top of z-stack. */
3451 focus(win) {
3452 const previouslyFocused = this._stack.length > 0 ? this._stack[this._stack.length - 1] : null;
3453 const priorFullscreen = this._stack.find(
3454 (w) => w !== win && w.isFocused() && w.isFullscreen()
3455 );
3456 if (priorFullscreen) {
3457 const shouldExit = applyFilters(
3458 HOOKS.WINDOW_AUTO_EXIT_FULLSCREEN,
3459 true,
3460 { windowId: priorFullscreen.id, focusedTo: win.id }
3461 );
3462 if (shouldExit) {
3463 priorFullscreen.toggleFullscreen();
3464 }
3465 }
3466 const idx = this._stack.indexOf(win);
3467 if (idx > -1) {
3468 this._stack.splice(idx, 1);
3469 }
3470 this._stack.push(win);
3471 this._stack.forEach((w, i) => {
3472 w.setZIndex(BASE_Z_INDEX + i);
3473 w.setFocused(i === this._stack.length - 1);
3474 });
3475 if (previouslyFocused && previouslyFocused !== win && previouslyFocused.id !== win.id) {
3476 const blurredDetail = {
3477 windowId: previouslyFocused.id,
3478 focusedTo: win.id
3479 };
3480 document.dispatchEvent(
3481 new CustomEvent("desktop-mode-window-blurred", { detail: blurredDetail })
3482 );
3483 doAction(HOOKS.WINDOW_BLURRED, blurredDetail);
3484 }
3485 const focusedDetail = { windowId: win.id };
3486 document.dispatchEvent(
3487 new CustomEvent("desktop-mode-window-focused", { detail: focusedDetail })
3488 );
3489 doAction(HOOKS.WINDOW_FOCUSED, focusedDetail);
3490 }
3491 /**
3492 * Raise a window to just below the top of the stack WITHOUT
3493 * changing focus — the focused window stays on top and keeps
3494 * keyboard/visual focus; the raised window surfaces above
3495 * everything else. No focus/blur events fire (this is a silent
3496 * restack, not a focus change).
3497 *
3498 * Used by the window-links feature to bring a relation group
3499 * forward when one of its members is focused; available to
3500 * plugins for any "surface my companion window" affordance.
3501 *
3502 * @since 0.9.4
3503 *
3504 * @param windowId Window to raise. Unknown ids and the focused
3505 * window itself are no-ops.
3506 */
3507 raise(windowId) {
3508 const win = this.getById(windowId);
3509 if (!win || this._stack.length < 2) {
3510 return;
3511 }
3512 const idx = this._stack.indexOf(win);
3513 if (idx === -1 || idx === this._stack.length - 1) {
3514 return;
3515 }
3516 this._stack.splice(idx, 1);
3517 this._stack.splice(this._stack.length - 1, 0, win);
3518 this._stack.forEach((w, i) => {
3519 w.setZIndex(BASE_Z_INDEX + i);
3520 });
3521 }
3522 /** Remove a window from the stack and DOM. */
3523 remove(win) {
3524 const idx = this._stack.indexOf(win);
3525 if (idx > -1) {
3526 this._stack.splice(idx, 1);
3527 }
3528 for (let i = this._stack.length - 1; i >= 0; i--) {
3529 const candidate = this._stack[i];
3530 if (candidate.state === "minimized") {
3531 continue;
3532 }
3533 const candidateDesktop = candidate.config.desktopId || this._activeDesktopId;
3534 if (candidateDesktop !== this._activeDesktopId) {
3535 continue;
3536 }
3537 this.focus(candidate);
3538 break;
3539 }
3540 const closingDetail = { windowId: win.id, element: win.element };
3541 document.dispatchEvent(
3542 new CustomEvent("desktop-mode-window-closing", { detail: closingDetail })
3543 );
3544 doAction(HOOKS.WINDOW_CLOSING, closingDetail);
3545 const closedDetail = { windowId: win.id };
3546 document.dispatchEvent(
3547 new CustomEvent("desktop-mode-window-closed", { detail: closedDetail })
3548 );
3549 doAction(HOOKS.WINDOW_CLOSED, closedDetail);
3550 }
3551 /** Get a window by its ID. */
3552 getById(id) {
3553 return this._stack.find((w) => w.id === id);
3554 }
3555 /**
3556 * Get the most-recently-focused window for a given baseId.
3557 *
3558 * Multi-instance windows share a baseId; the stack is ordered
3559 * bottom to top by focus, so iterating from the end finds the
3560 * best candidate to bring forward when the user re-clicks the
3561 * dock icon.
3562 */
3563 getByBaseId(baseId) {
3564 for (let i = this._stack.length - 1; i >= 0; i--) {
3565 const w = this._stack[i];
3566 if ((w.config.baseId || w.id) === baseId) {
3567 return w;
3568 }
3569 }
3570 return void 0;
3571 }
3572 /**
3573 * Like {@link getByBaseId} but only considers windows on the
3574 * currently-active virtual desktop. The dock's "open or focus"
3575 * path uses this — a Plugins instance that lives on Desktop 2 is
3576 * invisible from Desktop 1's dock click, so clicking Plugins on
3577 * Desktop 1 should open a fresh instance there instead of trying
3578 * to focus the far-off sibling (which would silently do nothing
3579 * because the other desktop's windows are display: none here).
3580 */
3581 getByBaseIdOnActiveDesktop(baseId) {
3582 for (let i = this._stack.length - 1; i >= 0; i--) {
3583 const w = this._stack[i];
3584 if ((w.config.baseId || w.id) !== baseId) {
3585 continue;
3586 }
3587 const winDesktop = w.config.desktopId || this._activeDesktopId;
3588 if (winDesktop === this._activeDesktopId) {
3589 return w;
3590 }
3591 }
3592 return void 0;
3593 }
3594 /**
3595 * Get every open window sharing the given baseId, ordered by
3596 * instance slot (bare baseId first, then `-2`, `-3`, …) rather
3597 * than z-order — so the dock's instance rail keeps a stable
3598 * left-to-right order even as the user focuses between windows.
3599 */
3600 getAllByBaseId(baseId) {
3601 const instanceSlot = (id) => {
3602 if (id === baseId) {
3603 return 1;
3604 }
3605 const prefix = `${baseId}-`;
3606 if (id.startsWith(prefix)) {
3607 const n = parseInt(id.slice(prefix.length), 10);
3608 return Number.isFinite(n) ? n : 999;
3609 }
3610 return 999;
3611 };
3612 return this._stack.filter((w) => (w.config.baseId || w.id) === baseId).sort((a, b) => instanceSlot(a.id) - instanceSlot(b.id));
3613 }
3614 /**
3615 * Get every open window sharing the given baseId on the active desktop,
3616 * ordered by instance slot.
3617 */
3618 getAllByBaseIdOnActiveDesktop(baseId) {
3619 return this.getAllByBaseId(baseId).filter(
3620 (w) => (w.config.desktopId || this._activeDesktopId) === this._activeDesktopId
3621 );
3622 }
3623 /** Get all open windows. */
3624 getAll() {
3625 return [...this._stack];
3626 }
3627 /**
3628 * Find the window whose iframe's contentWindow matches the given
3629 * message source. Used by cross-frame bridges to attribute inbound
3630 * `postMessage` events to the originating window without reaching
3631 * into `_stack`.
3632 */
3633 findByIframeSource(source) {
3634 if (!source) {
3635 return void 0;
3636 }
3637 return this._stack.find(
3638 (w) => w.iframe !== null && w.iframe.contentWindow === source
3639 );
3640 }
3641 /** Get the currently focused (topmost) window. */
3642 getFocused() {
3643 return this._stack.length > 0 ? this._stack[this._stack.length - 1] : void 0;
3644 }
3645 /**
3646 * "Is the window with this id currently in front of the user?"
3647 *
3648 * Returns true when the window exists in the manager AND it
3649 * isn't minimized AND it's the currently focused (topmost)
3650 * window. False otherwise — including for unknown ids, closed
3651 * windows, minimized windows, or windows that exist but aren't
3652 * on top.
3653 *
3654 * The canonical query for plugins implementing the "show
3655 * something *only when the user can't already see my
3656 * window*" pattern (badge counts, attention pulses, sounds,
3657 * toasts). Plugins that previously hand-rolled
3658 * `getById(id) && state !== 'minimized' && focused` can
3659 * collapse to this.
3660 *
3661 * @since 0.5.5
3662 *
3663 * @param id Window id to query.
3664 * @return True when the user is actively looking at this window.
3665 */
3666 isActive(id) {
3667 const win = this.getById(id);
3668 if (!win) {
3669 return false;
3670 }
3671 if (win.state === "minimized") {
3672 return false;
3673 }
3674 const winDesktop = win.config.desktopId || this._activeDesktopId;
3675 if (winDesktop !== this._activeDesktopId) {
3676 return false;
3677 }
3678 const focused = this.getFocused();
3679 return !!focused && focused.id === id;
3680 }
3681 /**
3682 * Like {@link isActive}, but returns true if *any* window with the
3683 * given baseId is currently active.
3684 */
3685 isActiveByBaseId(baseId) {
3686 const focused = this.getFocused();
3687 if (!focused) {
3688 return false;
3689 }
3690 if (focused.state === "minimized") {
3691 return false;
3692 }
3693 const winDesktop = focused.config.desktopId || this._activeDesktopId;
3694 if (winDesktop !== this._activeDesktopId) {
3695 return false;
3696 }
3697 return (focused.config.baseId || focused.id) === baseId;
3698 }
3699 // ---- Virtual desktop delegations ----
3700 getDesktops() {
3701 return getDesktops(this);
3702 }
3703 getActiveDesktop() {
3704 return getActiveDesktop(this);
3705 }
3706 getActiveDesktopId() {
3707 return getActiveDesktopId(this);
3708 }
3709 createDesktop() {
3710 return createDesktop(this);
3711 }
3712 switchDesktop(id, opts) {
3713 switchDesktop(this, id, opts);
3714 }
3715 closeDesktop(id) {
3716 closeDesktop(this, id);
3717 }
3718 /**
3719 * Returns the "primary" desktop id — the one new sessions land on
3720 * and that batch operations like {@link closeAll} treat as the
3721 * survivor when an `onlyOnPrimary` mode is requested.
3722 *
3723 * Default: the first desktop in `getDesktops()`. Filterable via
3724 * `desktop-mode.primary-desktop-id` so downstream code that wants a
3725 * different convention (e.g. a pinned "Inbox" desktop) can override
3726 * without having to fork the manager.
3727 *
3728 * @since 0.5.0
3729 */
3730 getPrimaryDesktopId() {
3731 const all2 = this.getDesktops();
3732 const fallback = all2.length > 0 ? all2[0].id : "desktop-1";
3733 const filtered = applyFilters(
3734 HOOKS.PRIMARY_DESKTOP_ID,
3735 fallback,
3736 all2
3737 );
3738 if (typeof filtered !== "string" || filtered === "") {
3739 return fallback;
3740 }
3741 const exists = all2.some((d) => d.id === filtered);
3742 return exists ? filtered : fallback;
3743 }
3744 /**
3745 * Close every open window in batch.
3746 *
3747 * Hook chain:
3748 *
3749 * 1. `desktop-mode.windows.before-close-all` — action. Subscribers
3750 * can prepare for the wipe (cancel pending saves, dismiss
3751 * menus, etc.). Detail: `{ candidates: Window[] }`.
3752 *
3753 * 2. `desktop-mode.windows.close-all` — filter. Receives the
3754 * candidate Window list and returns the (possibly smaller) list
3755 * that will actually be closed. Plugins use this to PROTECT
3756 * specific windows — e.g. keep a draft post window open during
3757 * a "Close all" operation. Returning an empty array cancels
3758 * the close entirely.
3759 *
3760 * 3. Each surviving window's `close()` is called.
3761 *
3762 * 4. `desktop-mode.windows.after-close-all` — action. Detail:
3763 * `{ closed: number, skipped: Window[] }`.
3764 *
3765 * @since 0.5.0
3766 *
3767 * @param options Close options.
3768 * @param options.exceptIds Window ids to skip even before the filter runs.
3769 * @return Number of windows actually closed.
3770 */
3771 closeAll(options) {
3772 const exceptSet = new Set(options?.exceptIds ?? []);
3773 const initialCandidates = this._stack.filter(
3774 (w) => !exceptSet.has(w.id)
3775 );
3776 doAction(HOOKS.WINDOWS_BEFORE_CLOSE_ALL, { candidates: initialCandidates });
3777 const filtered = applyFilters(
3778 HOOKS.WINDOWS_CLOSE_ALL,
3779 initialCandidates
3780 );
3781 const finalList = Array.isArray(filtered) ? filtered : initialCandidates;
3782 const skipped = initialCandidates.filter((w) => !finalList.includes(w));
3783 let closed = 0;
3784 for (const win of finalList.slice()) {
3785 try {
3786 win.close();
3787 closed++;
3788 } catch (err) {
3789 if (typeof console !== "undefined") {
3790 console.error(
3791 "[desktop-mode] closeAll: window.close() threw for",
3792 win.id,
3793 err
3794 );
3795 }
3796 }
3797 }
3798 doAction(HOOKS.WINDOWS_AFTER_CLOSE_ALL, { closed, skipped });
3799 return closed;
3800 }
3801 /**
3802 * Minimize every currently-non-minimized window. Returns the
3803 * exact set that was minimized — i.e., excludes windows already
3804 * in the `'minimized'` state — so callers can pair the call with
3805 * a later {@link restoreFrom} that touches only the windows
3806 * they minimized.
3807 *
3808 * The "Show Desktop" gesture (clicking the wallpaper) routes
3809 * through this method (and {@link restoreFrom} on the second
3810 * click); plugin authors building expand/collapse UIs that
3811 * mimic the gesture should use these primitives instead of
3812 * rolling the loop themselves.
3813 *
3814 * @public
3815 * @since 0.6.0
3816 */
3817 minimizeAll() {
3818 const minimized = [];
3819 for (const win of this._stack.slice()) {
3820 const winDesktop = win.config.desktopId || this._activeDesktopId;
3821 if (winDesktop !== this._activeDesktopId) {
3822 continue;
3823 }
3824 if (win.state === "minimized") {
3825 continue;
3826 }
3827 try {
3828 win.minimize();
3829 minimized.push(win);
3830 } catch (err) {
3831 if (typeof console !== "undefined") {
3832 console.error(
3833 "[desktop-mode] minimizeAll: window.minimize() threw for",
3834 win.id,
3835 err
3836 );
3837 }
3838 }
3839 }
3840 return minimized;
3841 }
3842 /**
3843 * Restore the given window list — the symmetric counterpart to
3844 * {@link minimizeAll}. Skips windows that have since been
3845 * closed and windows the user manually un-minimized between
3846 * the minimize and the restore.
3847 *
3848 * Pass the array {@link minimizeAll} returned to restore
3849 * exactly what you minimized; pass any subset to restore
3850 * selectively.
3851 *
3852 * @public
3853 * @since 0.6.0
3854 */
3855 restoreFrom(windows) {
3856 if (!Array.isArray(windows)) {
3857 return;
3858 }
3859 const live = new Set(this._stack);
3860 for (const win of windows) {
3861 if (!live.has(win)) {
3862 continue;
3863 }
3864 const winDesktop = win.config.desktopId || this._activeDesktopId;
3865 if (winDesktop !== this._activeDesktopId) {
3866 continue;
3867 }
3868 if (win.state !== "minimized") {
3869 continue;
3870 }
3871 try {
3872 win.restore();
3873 } catch (err) {
3874 if (typeof console !== "undefined") {
3875 console.error(
3876 "[desktop-mode] restoreFrom: window.restore() threw for",
3877 win.id,
3878 err
3879 );
3880 }
3881 }
3882 }
3883 }
3884 /**
3885 * Toggle the "Show Desktop" state — if every live window is
3886 * already minimized, restore them all; otherwise minimize the
3887 * non-minimized cohort. Returns `true` when the new state is
3888 * "showing the desktop" (everything minimized after the call),
3889 * `false` when windows have just been restored.
3890 *
3891 * Mirrors the wallpaper-click gesture exactly, in one call.
3892 *
3893 * @public
3894 * @since 0.6.0
3895 */
3896 toggleShowDesktop() {
3897 const all2 = this._stack.filter(
3898 (w) => (w.config.desktopId || this._activeDesktopId) === this._activeDesktopId
3899 );
3900 if (all2.length === 0) {
3901 return false;
3902 }
3903 const allMinimized = all2.every((w) => w.state === "minimized");
3904 if (allMinimized) {
3905 for (const win of all2) {
3906 try {
3907 win.restore();
3908 } catch {
3909 }
3910 }
3911 return false;
3912 }
3913 this.minimizeAll();
3914 return true;
3915 }
3916 // ---- Arrange + snap delegations ----
3917 cascade() {
3918 cascade(this);
3919 }
3920 tile() {
3921 tile(this);
3922 }
3923 isSnapEnabled() {
3924 return this._snapEnabled;
3925 }
3926 setSnapEnabled(enabled) {
3927 setSnapEnabled(this, enabled);
3928 }
3929 getSnapConfig() {
3930 return getSnapConfig(this);
3931 }
3932 // ---- Overview delegations ----
3933 enterOverview() {
3934 enterOverview(this);
3935 }
3936 exitOverview(selected, maximize = false) {
3937 exitOverview(this, selected, maximize);
3938 }
3939 /**
3940 * Release resources this instance owns outside its own DOM
3941 * subtree: the document-level overview key handler and any
3942 * pending overview transition timers. Removing `desktop` from the
3943 * DOM does not reach either of those — a caller discarding a
3944 * manager instance (tests; a future SPA-style unmount) that skips
3945 * this leaves a real `setTimeout` to fire later and reach for
3946 * globals that may already be gone, plus a `keydown` listener on
3947 * `document` that keeps responding on behalf of a manager nothing
3948 * else references.
3949 *
3950 * Safe to call unconditionally — a no-op when overview was never
3951 * entered or was already cleanly exited.
3952 */
3953 destroy() {
3954 if (this._overviewActive) {
3955 exitOverview(this);
3956 }
3957 cancelOverviewTimers(this);
3958 }
3959 /**
3960 * Snapshot every open window's current geometry + state.
3961 *
3962 * Returns a plain array of `{ windowId, rect, state, element }`
3963 * entries — one per window in the stack, regardless of which
3964 * virtual desktop owns it. Rect coordinates are in desktop-area
3965 * space (the same coordinate space the windows themselves use
3966 * inline-style left/top); `state` is the live `WindowState`, and
3967 * `element` is the window's outer DOM node.
3968 *
3969 * Intended for wallpaper / overlay plugins that used to scrape
3970 * `document.querySelectorAll('.desktop-mode-window')` + read the
3971 * `--minimized` / `--maximized` modifier classes by name. The
3972 * accessor decouples plugin code from the shell's CSS class
3973 * naming, so a future refactor of modifier prefixes is not an
3974 * ecosystem break.
3975 *
3976 * The array contains every window in the stack — callers filter
3977 * on `state` if they want only "actually visible" (typically
3978 * `state !== 'minimized'`). Minimized windows are included so
3979 * plugins that care about the "will be restored to X geometry"
3980 * case still have the data; filtering them out would be a
3981 * subtraction the caller can do but the provider can't reverse.
3982 *
3983 * Order matches the internal z-stack: earliest-opened first,
3984 * focused window last.
3985 */
3986 getVisibleRects() {
3987 return this._stack.map((w) => {
3988 const snap = w.getSnapshot();
3989 return {
3990 windowId: w.id,
3991 rect: {
3992 x: snap.x,
3993 y: snap.y,
3994 width: snap.width,
3995 height: snap.height
3996 },
3997 state: snap.state,
3998 element: w.element
3999 };
4000 });
4001 }
4002 /**
4003 * Serialize the current window stack for session persistence.
4004 *
4005 * Order in the returned `windows` array mirrors z-order (earliest
4006 * opened / lowest-z first, focused last) so restoring preserves
4007 * the stacking the user left behind.
4008 */
4009 snapshot() {
4010 const focused = this.getFocused();
4011 const persistable = this._stack.filter((w) => !w.config.native);
4012 const windows = persistable.map((w) => {
4013 const snap = w.getSnapshot();
4014 const externalTabs = w.getExternalTabsSnapshot();
4015 return {
4016 id: w.id,
4017 baseId: w.config.baseId || w.id,
4018 desktopId: w.config.desktopId || this._activeDesktopId,
4019 url: w.getCurrentUrl(),
4020 title: w.config.title,
4021 icon: w.config.icon,
4022 state: snap.state,
4023 x: snap.x,
4024 y: snap.y,
4025 width: snap.width,
4026 height: snap.height,
4027 ...externalTabs.length > 0 ? { externalTabs } : {}
4028 };
4029 });
4030 const focusedId = focused && !focused.config.native ? focused.id : "";
4031 return {
4032 windows,
4033 desktops: this.getDesktops(),
4034 activeDesktop: this._activeDesktopId,
4035 focused: focusedId,
4036 updated: Math.floor(Date.now() / 1e3)
4037 };
4038 }
4039 seedDesktops(desktops, activeDesktopId) {
4040 seedDesktops(this, desktops, activeDesktopId);
4041 }
4042 }
4043 function cycleableWindows(mgr) {
4044 const activeDesktopId = mgr.getActiveDesktopId();
4045 const domOrder = Array.from(mgr._desktop.children);
4046 return mgr.getAll().filter((w) => {
4047 const winDesktop = w.config.desktopId || activeDesktopId;
4048 return winDesktop === activeDesktopId;
4049 }).sort(
4050 (a, b) => domOrder.indexOf(a.element) - domOrder.indexOf(b.element)
4051 );
4052 }
4053 function cycleFocus(mgr, direction) {
4054 if (mgr._overviewActive) {
4055 return;
4056 }
4057 const list2 = cycleableWindows(mgr);
4058 if (list2.length < 2) {
4059 return;
4060 }
4061 const focused = mgr.getFocused();
4062 const currentIdx = focused ? list2.indexOf(focused) : -1;
4063 const step = direction === "next" ? 1 : -1;
4064 const nextIdx = (currentIdx + step + list2.length) % list2.length;
4065 const target2 = list2[nextIdx];
4066 if (target2.state === "minimized") {
4067 target2.restore();
4068 } else {
4069 mgr.focus(target2);
4070 }
4071 }
4072 let installed$3 = false;
4073 function isTextEntryFocus(doc) {
4074 let el = doc.activeElement;
4075 while (el && el.shadowRoot && el.shadowRoot.activeElement) {
4076 el = el.shadowRoot.activeElement;
4077 }
4078 if (!el) {
4079 return false;
4080 }
4081 if (el instanceof HTMLIFrameElement) {
4082 return true;
4083 }
4084 if (el instanceof HTMLTextAreaElement) {
4085 return true;
4086 }
4087 if (el instanceof HTMLInputElement) {
4088 const textTypes = /* @__PURE__ */ new Set([
4089 "text",
4090 "search",
4091 "url",
4092 "email",
4093 "password",
4094 "tel",
4095 "number",
4096 "date",
4097 "datetime-local",
4098 "month",
4099 "week",
4100 "time"
4101 ]);
4102 return textTypes.has(el.type);
4103 }
4104 if (el instanceof HTMLElement && el.isContentEditable === true) {
4105 return true;
4106 }
4107 const ce = el.getAttribute("contenteditable");
4108 return ce !== null && ce !== "false";
4109 }
4110 function installWindowSwitcherShortcut(mgr) {
4111 if (installed$3) {
4112 return;
4113 }
4114 installed$3 = true;
4115 document.addEventListener(
4116 "keydown",
4117 (e) => {
4118 if (e.ctrlKey || e.metaKey || e.altKey) {
4119 return;
4120 }
4121 if (e.code !== "Backquote") {
4122 return;
4123 }
4124 if (isTextEntryFocus(document)) {
4125 return;
4126 }
4127 e.preventDefault();
4128 cycleFocus(mgr, e.shiftKey ? "prev" : "next");
4129 },
4130 true
4131 );
4132 const origin = window.location.origin;
4133 window.addEventListener("message", (e) => {
4134 if (e.origin !== origin) {
4135 return;
4136 }
4137 const data = e.data;
4138 if (!data || data.type !== "desktop-mode-window-switch") {
4139 return;
4140 }
4141 cycleFocus(mgr, data.direction === "prev" ? "prev" : "next");
4142 });
4143 }
4144 function switchToAdjacentDesktop(mgr, direction) {
4145 const desktops = mgr.getDesktops();
4146 if (desktops.length < 2) {
4147 return false;
4148 }
4149 const activeId = mgr.getActiveDesktopId();
4150 const idx = desktops.findIndex((d) => d.id === activeId);
4151 if (idx === -1) {
4152 return false;
4153 }
4154 const step = direction === "next" ? 1 : -1;
4155 const targetIdx = (idx + step + desktops.length) % desktops.length;
4156 if (targetIdx === idx) {
4157 return false;
4158 }
4159 mgr.switchDesktop(desktops[targetIdx].id, { direction });
4160 return true;
4161 }
4162 function cycleOverviewCursor(mgr, direction) {
4163 if (!mgr._overviewActive) {
4164 return false;
4165 }
4166 const desktops = mgr.getDesktops();
4167 const cycleLength = desktops.length + 1;
4168 const ADD_INDEX = desktops.length;
4169 const currentIdx = mgr._overviewAddTileFocused ? ADD_INDEX : desktops.findIndex((d) => d.id === mgr.getActiveDesktopId());
4170 if (currentIdx === -1) {
4171 return false;
4172 }
4173 const step = direction === "next" ? 1 : -1;
4174 const targetIdx = (currentIdx + step + cycleLength) % cycleLength;
4175 if (targetIdx === currentIdx) {
4176 return false;
4177 }
4178 if (targetIdx === ADD_INDEX) {
4179 mgr._overviewAddTileFocused = true;
4180 refreshOverviewTopBar(mgr);
4181 return true;
4182 }
4183 mgr._overviewAddTileFocused = false;
4184 mgr.switchDesktop(desktops[targetIdx].id, { direction });
4185 return true;
4186 }
4187 function toggleOverview(mgr) {
4188 if (mgr._overviewActive) {
4189 mgr.exitOverview();
4190 } else {
4191 mgr.enterOverview();
4192 }
4193 return true;
4194 }
4195 function toggleShowDesktop(mgr) {
4196 if (mgr._overviewActive) {
4197 return false;
4198 }
4199 if (mgr.getAll().length === 0) {
4200 return false;
4201 }
4202 mgr.toggleShowDesktop();
4203 return true;
4204 }
4205 function exitOverviewIfActive(mgr) {
4206 if (!mgr._overviewActive) {
4207 return false;
4208 }
4209 mgr.exitOverview();
4210 return true;
4211 }
4212 function isShowDesktopActive(mgr) {
4213 const all2 = mgr.getAll();
4214 if (all2.length === 0) {
4215 return false;
4216 }
4217 return all2.every((w) => w.state === "minimized");
4218 }
4219 function exitShowDesktopIfActive(mgr) {
4220 if (!isShowDesktopActive(mgr)) {
4221 return false;
4222 }
4223 mgr.toggleShowDesktop();
4224 return true;
4225 }
4226 let installed$2 = false;
4227 function installDesktopArrowShortcuts(mgr) {
4228 if (installed$2) {
4229 return;
4230 }
4231 installed$2 = true;
4232 document.addEventListener(
4233 "keydown",
4234 (e) => {
4235 if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) {
4236 return;
4237 }
4238 if (e.code !== "ArrowLeft" && e.code !== "ArrowRight" && e.code !== "ArrowUp" && e.code !== "ArrowDown") {
4239 return;
4240 }
4241 if (isTextEntryFocus(document)) {
4242 return;
4243 }
4244 let handled = false;
4245 switch (e.code) {
4246 case "ArrowLeft":
4247 handled = mgr._overviewActive ? cycleOverviewCursor(mgr, "prev") : switchToAdjacentDesktop(mgr, "prev");
4248 break;
4249 case "ArrowRight":
4250 handled = mgr._overviewActive ? cycleOverviewCursor(mgr, "next") : switchToAdjacentDesktop(mgr, "next");
4251 break;
4252 case "ArrowUp":
4253 handled = exitOverviewIfActive(mgr) || exitShowDesktopIfActive(mgr) || toggleOverview(mgr);
4254 break;
4255 case "ArrowDown":
4256 handled = exitOverviewIfActive(mgr) || toggleShowDesktop(mgr);
4257 break;
4258 }
4259 if (handled) {
4260 e.preventDefault();
4261 }
4262 },
4263 true
4264 );
4265 }
4266 function hashTitleToHue(input) {
4267 if (!input) {
4268 return 214;
4269 }
4270 let hash2 = 5381;
4271 for (let i = 0; i < input.length; i++) {
4272 hash2 = Math.imul(hash2, 33) + input.charCodeAt(i);
4273 }
4274 return (hash2 % 360 + 360) % 360;
4275 }
4276 function renderIcon(icon, opts) {
4277 const className = opts.className ?? "";
4278 const title = opts.title ?? "";
4279 if (typeof icon === "string" && icon.startsWith("dashicons-")) {
4280 const el = document.createElement("span");
4281 el.className = `dashicons ${icon} ${className}`.trim();
4282 el.setAttribute("aria-hidden", "true");
4283 return el;
4284 }
4285 if (typeof icon === "string" && icon.startsWith("data:image/svg+xml;base64,")) {
4286 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
4287 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
4288 const el = document.createElement("span");
4289 el.className = className;
4290 el.setAttribute("aria-hidden", "true");
4291 el.style.backgroundImage = `url("${icon}")`;
4292 el.style.backgroundRepeat = "no-repeat";
4293 el.style.backgroundPosition = "center";
4294 el.style.backgroundSize = "contain";
4295 el.style.display = "inline-block";
4296 return el;
4297 }
4298 }
4299 if (typeof icon === "string" && /^data:image\/(png|jpeg|jpg|gif|webp|x-icon|vnd\.microsoft\.icon);base64,/i.test(icon)) {
4300 const commaIdx = icon.indexOf(",");
4301 const payload = commaIdx >= 0 ? icon.slice(commaIdx + 1) : "";
4302 if (/^[A-Za-z0-9+/=]+$/.test(payload)) {
4303 return makeImgIcon(icon, className);
4304 }
4305 }
4306 if (typeof icon === "string" && (icon.startsWith("http://") || icon.startsWith("https://"))) {
4307 return makeImgIcon(icon, className);
4308 }
4309 const span = document.createElement("span");
4310 span.className = `${className} desktop-mode-icon-letter`.trim();
4311 span.setAttribute("aria-hidden", "true");
4312 const letters = letterFromTitle(title);
4313 span.textContent = letters;
4314 const hue = hashTitleToHue(title);
4315 span.style.backgroundColor = `hsl( ${hue}, 60%, 45% )`;
4316 span.style.color = "#fff";
4317 span.style.display = "inline-flex";
4318 span.style.alignItems = "center";
4319 span.style.justifyContent = "center";
4320 span.style.fontWeight = "600";
4321 span.style.borderRadius = "4px";
4322 return span;
4323 }
4324 function makeImgIcon(src, className) {
4325 const img = document.createElement("img");
4326 img.className = className;
4327 img.src = src;
4328 img.alt = "";
4329 img.setAttribute("aria-hidden", "true");
4330 img.draggable = false;
4331 return img;
4332 }
4333 function letterFromTitle(title) {
4334 const trimmed = (title ?? "").trim();
4335 if (trimmed === "") {
4336 return "?";
4337 }
4338 const words = trimmed.split(/\s+/);
4339 if (words.length >= 2) {
4340 return (words[0][0] + words[1][0]).toUpperCase();
4341 }
4342 const first = words[0];
4343 if (first.length >= 2) {
4344 return first.slice(0, 2).toUpperCase();
4345 }
4346 return first.toUpperCase();
4347 }
4348 const _parentSubs = /* @__PURE__ */ new Map();
4349 const _nativeSubs = /* @__PURE__ */ new Map();
4350 function bucket(root, windowId, channel, create) {
4351 let perWindow = root.get(windowId);
4352 if (!perWindow) {
4353 if (!create) {
4354 return void 0;
4355 }
4356 perWindow = /* @__PURE__ */ new Map();
4357 root.set(windowId, perWindow);
4358 }
4359 let bucketSet = perWindow.get(channel);
4360 if (!bucketSet) {
4361 if (!create) {
4362 return void 0;
4363 }
4364 bucketSet = /* @__PURE__ */ new Set();
4365 perWindow.set(channel, bucketSet);
4366 }
4367 return bucketSet;
4368 }
4369 function dispatch(root, windowId, channel, payload) {
4370 const meta = { channel, windowId };
4371 const exact = bucket(root, windowId, channel, false);
4372 if (exact) {
4373 for (const cb of Array.from(exact)) {
4374 try {
4375 cb(payload, meta);
4376 } catch (err) {
4377 if (typeof console !== "undefined") {
4378 console.error(
4379 `[desktop-mode] window-channel subscriber for "${channel}" threw:`,
4380 err
4381 );
4382 }
4383 }
4384 }
4385 }
4386 const wildcard = bucket(root, windowId, "*", false);
4387 if (wildcard) {
4388 for (const cb of Array.from(wildcard)) {
4389 try {
4390 cb(payload, meta);
4391 } catch (err) {
4392 if (typeof console !== "undefined") {
4393 console.error(
4394 `[desktop-mode] window-channel wildcard subscriber for "${windowId}" threw:`,
4395 err
4396 );
4397 }
4398 }
4399 }
4400 }
4401 }
4402 function addParentSubscriber(windowId, channel, cb) {
4403 const set = bucket(_parentSubs, windowId, channel, true);
4404 set.add(cb);
4405 let removed = false;
4406 return () => {
4407 if (removed) {
4408 return;
4409 }
4410 removed = true;
4411 set.delete(cb);
4412 };
4413 }
4414 function dispatchFromWindow(windowId, channel, payload) {
4415 dispatch(_parentSubs, windowId, channel, payload);
4416 }
4417 function dispatchToNative(windowId, channel, payload) {
4418 dispatch(_nativeSubs, windowId, channel, payload);
4419 }
4420 const _readyWindows = /* @__PURE__ */ new Set();
4421 const _loadingWindows = /* @__PURE__ */ new Set();
4422 const _pendingSends = /* @__PURE__ */ new Map();
4423 function markWindowContentReady(windowId) {
4424 if (!_readyWindows.has(windowId)) {
4425 _readyWindows.add(windowId);
4426 const queued = _pendingSends.get(windowId);
4427 if (queued) {
4428 _pendingSends.delete(windowId);
4429 for (const m of queued) {
4430 try {
4431 m.flush();
4432 } catch (err) {
4433 if (typeof console !== "undefined") {
4434 console.error(
4435 `[desktop-mode] flushing queued window-send for "${m.channel}" threw:`,
4436 err
4437 );
4438 }
4439 }
4440 }
4441 }
4442 }
4443 if (_loadingWindows.delete(windowId)) {
4444 doAction(HOOKS.WINDOW_CONTENT_LOADED, { windowId });
4445 if (typeof document !== "undefined") {
4446 document.dispatchEvent(
4447 new CustomEvent("desktop-mode-window-content-loaded", {
4448 detail: { windowId }
4449 })
4450 );
4451 }
4452 }
4453 }
4454 const WINDOW_CONFIG_KEY = Symbol.for("desktop-mode/window-config");
4455 function getWindowConfigFromElement(el) {
4456 return el[WINDOW_CONFIG_KEY];
4457 }
4458 function buildDefaultLoadingOverlay() {
4459 const overlay = document.createElement("div");
4460 overlay.className = "desktop-mode-window__loading";
4461 overlay.setAttribute("aria-hidden", "true");
4462 const spinner = document.createElement("wpd-spinner");
4463 spinner.setAttribute("preset", "classic");
4464 spinner.setAttribute("size", "clamp(96px, 14vw, 192px)");
4465 spinner.setAttribute("label", __("Loading window content"));
4466 overlay.appendChild(spinner);
4467 return overlay;
4468 }
4469 function createLoadingOverlay(config) {
4470 let overlay = buildDefaultLoadingOverlay();
4471 const ctx = { windowId: config.id, config };
4472 if (typeof config.loading?.render === "function") {
4473 try {
4474 config.loading.render(overlay, ctx);
4475 } catch (err) {
4476 if (typeof console !== "undefined") {
4477 console.error(
4478 `[desktop-mode] loading.render threw for "${config.id}":`,
4479 err
4480 );
4481 }
4482 }
4483 }
4484 try {
4485 const filtered = applyFilters(
4486 HOOKS.WINDOW_LOADING_OVERLAY,
4487 overlay,
4488 ctx
4489 );
4490 if (filtered instanceof HTMLElement) {
4491 overlay = filtered;
4492 }
4493 } catch (err) {
4494 if (typeof console !== "undefined") {
4495 console.error(
4496 `[desktop-mode] WINDOW_LOADING_OVERLAY filter threw for "${config.id}":`,
4497 err
4498 );
4499 }
4500 }
4501 if (overlay && !overlay.classList.contains("desktop-mode-window__loading")) {
4502 overlay.classList.add("desktop-mode-window__loading");
4503 }
4504 return overlay;
4505 }
4506 function removeLoadingOverlay(windowEl) {
4507 const overlay = windowEl.querySelector(":scope .desktop-mode-window__loading");
4508 overlay?.remove();
4509 }
4510 function ensureLoadingOverlay(windowEl) {
4511 const body = windowEl.querySelector(
4512 ":scope .desktop-mode-window__body"
4513 );
4514 if (!body) {
4515 return;
4516 }
4517 const existing = body.querySelector(":scope .desktop-mode-window__loading");
4518 if (existing) {
4519 return;
4520 }
4521 const config = getWindowConfigFromElement(windowEl);
4522 body.appendChild(config ? createLoadingOverlay(config) : buildDefaultLoadingOverlay());
4523 }
4524 const FADE_OUT_MS$1 = 250;
4525 let _installed$5 = false;
4526 function findWindowElement(windowId) {
4527 if (!windowId) {
4528 return null;
4529 }
4530 return document.getElementById(`wp-window-${windowId}`);
4531 }
4532 function installWindowLoadingTransitions() {
4533 if (_installed$5) {
4534 return;
4535 }
4536 _installed$5 = true;
4537 _installSubscriptions();
4538 }
4539 function _installSubscriptions() {
4540 addAction(
4541 HOOKS.WINDOW_CONTENT_LOADING,
4542 "desktop-mode/window-loading-enter",
4543 (e) => {
4544 const el = findWindowElement(e?.windowId ?? "");
4545 if (!el) {
4546 return;
4547 }
4548 const body = el.querySelector(
4549 ":scope .desktop-mode-window__body"
4550 );
4551 if (!body) {
4552 return;
4553 }
4554 body.classList.add("desktop-mode-window__body--loading");
4555 ensureLoadingOverlay(el);
4556 }
4557 );
4558 addAction(
4559 HOOKS.WINDOW_CONTENT_LOADED,
4560 "desktop-mode/window-loading-exit",
4561 (e) => {
4562 const el = findWindowElement(e?.windowId ?? "");
4563 if (!el) {
4564 return;
4565 }
4566 const body = el.querySelector(
4567 ":scope .desktop-mode-window__body"
4568 );
4569 if (!body) {
4570 return;
4571 }
4572 body.classList.remove("desktop-mode-window__body--loading");
4573 window.setTimeout(() => {
4574 if (!body.classList.contains("desktop-mode-window__body--loading")) {
4575 removeLoadingOverlay(el);
4576 }
4577 }, FADE_OUT_MS$1);
4578 }
4579 );
4580 addAction(
4581 HOOKS.INIT,
4582 "desktop-mode/loading-overlay-init-sweep",
4583 () => {
4584 queueMicrotask(() => repaintLoadingOverlays());
4585 }
4586 );
4587 }
4588 function repaintLoadingOverlays() {
4589 const bodies = document.querySelectorAll(
4590 ".desktop-mode-window__body--loading"
4591 );
4592 bodies.forEach((body) => {
4593 const windowEl = body.closest(".desktop-mode-window");
4594 if (!windowEl) {
4595 return;
4596 }
4597 body.querySelector(":scope .desktop-mode-window__loading")?.remove();
4598 ensureLoadingOverlay(windowEl);
4599 });
4600 }
4601 const SHARED_STORES_SLOT = "__desktopModeSharedStores";
4602 function resolveSlot() {
4603 const w = window;
4604 let slot = w[SHARED_STORES_SLOT];
4605 if (!slot) {
4606 slot = /* @__PURE__ */ new Map();
4607 w[SHARED_STORES_SLOT] = slot;
4608 }
4609 return slot;
4610 }
4611 function createSharedStore(key, initialState) {
4612 const slot = resolveSlot();
4613 let record = slot.get(key);
4614 if (!record) {
4615 record = {
4616 state: initialState(),
4617 listeners: /* @__PURE__ */ new Set(),
4618 rebuild: initialState
4619 };
4620 slot.set(key, record);
4621 }
4622 const handle = {
4623 // `record.state` is the live reference. The getter on the
4624 // `state` field reads the latest value even if `reset()`
4625 // reassigned it to a fresh object.
4626 get state() {
4627 return record.state;
4628 },
4629 set state(next) {
4630 record.state = next;
4631 },
4632 getState() {
4633 return record.state;
4634 },
4635 notify() {
4636 for (const cb of Array.from(record.listeners)) {
4637 try {
4638 cb(record.state);
4639 } catch (err) {
4640 console.error(
4641 `[desktop-mode/shared-store:${key}] subscriber threw:`,
4642 err
4643 );
4644 }
4645 }
4646 },
4647 subscribe(cb) {
4648 record.listeners.add(cb);
4649 return () => {
4650 record.listeners.delete(cb);
4651 };
4652 },
4653 setState(patch) {
4654 const cur = record.state;
4655 if (typeof cur !== "object" || cur === null) {
4656 console.warn(
4657 `[desktop-mode/shared-store:${key}] setState called on a primitive store; use the state setter instead.`
4658 );
4659 return;
4660 }
4661 Object.assign(cur, patch);
4662 handle.notify();
4663 },
4664 reset() {
4665 const fresh = record.rebuild();
4666 const cur = record.state;
4667 if (typeof cur === "object" && cur !== null && typeof fresh === "object" && fresh !== null) {
4668 const target2 = cur;
4669 for (const k of Object.keys(target2)) {
4670 delete target2[k];
4671 }
4672 Object.assign(target2, fresh);
4673 } else {
4674 record.state = fresh;
4675 }
4676 record.listeners.clear();
4677 }
4678 };
4679 return handle;
4680 }
4681 const remapStore = createSharedStore(
4682 "desktop-mode/native-url-remap",
4683 () => ({ remaps: [], deps: null })
4684 );
4685 function bindNativeUrlRemap(bound) {
4686 remapStore.state.deps = bound;
4687 }
4688 function registerNativeUrlRemap(entry) {
4689 if (!entry || typeof entry.id !== "string" || entry.id.trim() === "") {
4690 return () => {
4691 };
4692 }
4693 if (typeof entry.nativeWindowId !== "string" || entry.nativeWindowId === "") {
4694 return () => {
4695 };
4696 }
4697 if (typeof entry.matches !== "function") {
4698 return () => {
4699 };
4700 }
4701 const remaps = remapStore.state.remaps;
4702 const existingIdx = remaps.findIndex((r) => r.id === entry.id);
4703 if (existingIdx >= 0) {
4704 remaps.splice(existingIdx, 1);
4705 }
4706 remaps.push(entry);
4707 return () => unregisterNativeUrlRemap(entry.id);
4708 }
4709 function unregisterNativeUrlRemap(id) {
4710 const remaps = remapStore.state.remaps;
4711 const i = remaps.findIndex((r) => r.id === id);
4712 if (i >= 0) {
4713 remaps.splice(i, 1);
4714 }
4715 }
4716 function resolveNativeUrlRemap(url) {
4717 const { deps: deps2, remaps } = remapStore.state;
4718 if (!deps2 || !url) {
4719 return null;
4720 }
4721 let parsed;
4722 try {
4723 parsed = new URL(url, deps2.adminUrl);
4724 } catch {
4725 return null;
4726 }
4727 const snapshot = deps2.getSnapshot();
4728 for (const entry of remaps) {
4729 if (!entry.matches(url, parsed)) {
4730 continue;
4731 }
4732 if (entry.enabled && !entry.enabled(snapshot)) {
4733 continue;
4734 }
4735 return entry.nativeWindowId;
4736 }
4737 return null;
4738 }
4739 function tryNativeUrlRemap(url) {
4740 const { deps: deps2, remaps } = remapStore.state;
4741 if (!deps2 || !url) {
4742 return false;
4743 }
4744 let parsed;
4745 try {
4746 parsed = new URL(url, deps2.adminUrl);
4747 } catch {
4748 return false;
4749 }
4750 const snapshot = deps2.getSnapshot();
4751 for (const entry of remaps) {
4752 if (!entry.matches(url, parsed)) {
4753 continue;
4754 }
4755 if (entry.enabled && !entry.enabled(snapshot)) {
4756 continue;
4757 }
4758 if (entry.onMatch) {
4759 try {
4760 entry.onMatch(url, parsed);
4761 } catch (err) {
4762 console.warn(
4763 `[desktop-mode] URL remap onMatch hook threw for "${entry.id}":`,
4764 err
4765 );
4766 }
4767 }
4768 if (deps2.openById(entry.nativeWindowId)) {
4769 return true;
4770 }
4771 }
4772 return false;
4773 }
4774 const HOOK_PREFIX = "desktop-mode.activity.";
4775 function hookName(channel) {
4776 return `${HOOK_PREFIX}${String(channel)}`;
4777 }
4778 let subscribeSeq = 0;
4779 const activity = {
4780 publish(channel, payload) {
4781 doAction(hookName(channel), payload);
4782 },
4783 subscribe(channel, cb) {
4784 const ns = `desktop-mode/activity-sub/${++subscribeSeq}`;
4785 const hook = hookName(channel);
4786 addAction(
4787 hook,
4788 ns,
4789 (payload) => cb(payload)
4790 );
4791 let removed = false;
4792 return () => {
4793 if (removed) {
4794 return;
4795 }
4796 removed = true;
4797 removeAction(hook, ns);
4798 };
4799 },
4800 filter(channel, value, ...args) {
4801 return applyFilters(hookName(channel), value, ...args);
4802 }
4803 };
4804 const DEFAULT_DURATION_MS = 4e3;
4805 const FADE_OUT_MS = 200;
4806 function showToast(options) {
4807 const intent = activity.filter(
4808 "desktop-mode/toast-requested",
4809 { ...options }
4810 );
4811 if (!intent || intent.cancel === true) {
4812 return () => void 0;
4813 }
4814 let dismissRequested = false;
4815 let realDismiss = null;
4816 openWithShellOverlays(
4817 () => !dismissRequested,
4818 () => {
4819 realDismiss = renderToast(intent);
4820 }
4821 );
4822 return () => {
4823 dismissRequested = true;
4824 if (realDismiss) {
4825 realDismiss();
4826 }
4827 };
4828 }
4829 function renderToast(intent) {
4830 const container = ensureContainer();
4831 const toast = document.createElement("wpd-toast");
4832 toast.textContent = intent.message;
4833 if (intent.action) {
4834 toast.setAttribute("action", intent.action.label);
4835 toast.addEventListener("wpd-toast-action", () => {
4836 intent.action?.onClick();
4837 dismiss();
4838 });
4839 }
4840 if (intent.dismissible) {
4841 toast.setAttribute("dismissible", "");
4842 toast.addEventListener("wpd-toast-dismiss", () => {
4843 intent.onDismiss?.();
4844 dismiss();
4845 });
4846 }
4847 container.appendChild(toast);
4848 let dismissed = false;
4849 let dismissTimer = null;
4850 const dismiss = () => {
4851 if (dismissed) {
4852 return;
4853 }
4854 dismissed = true;
4855 if (dismissTimer !== null) {
4856 window.clearTimeout(dismissTimer);
4857 dismissTimer = null;
4858 }
4859 toast.setAttribute("state", "out");
4860 window.setTimeout(() => {
4861 toast.remove();
4862 }, FADE_OUT_MS);
4863 };
4864 requestAnimationFrame(() => {
4865 toast.setAttribute("state", "in");
4866 });
4867 if (!intent.persistent) {
4868 dismissTimer = window.setTimeout(
4869 dismiss,
4870 intent.duration ?? DEFAULT_DURATION_MS
4871 );
4872 }
4873 activity.publish("desktop-mode/toast-shown", { ...intent });
4874 return dismiss;
4875 }
4876 function ensureContainer() {
4877 const existing = document.querySelector(
4878 "wpd-toast-container"
4879 );
4880 if (existing) {
4881 return existing;
4882 }
4883 const el = document.createElement("wpd-toast-container");
4884 document.body.appendChild(el);
4885 return el;
4886 }
4887 const store$j = createSharedStore(
4888 "desktop-mode/destructive-admin-actions",
4889 () => ({ entries: [] })
4890 );
4891 function registerDestructiveAdminAction(entry) {
4892 if (!entry || typeof entry.id !== "string" || entry.id.trim() === "") {
4893 return () => {
4894 };
4895 }
4896 if (typeof entry.matches !== "function") {
4897 return () => {
4898 };
4899 }
4900 const entries = store$j.state.entries;
4901 const idx = entries.findIndex((e) => e.id === entry.id);
4902 if (idx >= 0) {
4903 entries.splice(idx, 1);
4904 }
4905 entries.push(entry);
4906 return () => unregisterDestructiveAdminAction(entry.id);
4907 }
4908 function unregisterDestructiveAdminAction(id) {
4909 const entries = store$j.state.entries;
4910 const idx = entries.findIndex((e) => e.id === id);
4911 if (idx >= 0) {
4912 entries.splice(idx, 1);
4913 }
4914 }
4915 function listDestructiveAdminActions() {
4916 return store$j.state.entries.slice();
4917 }
4918 function collectRegistrationErrors(def, checks) {
4919 if (!def || typeof def !== "object") {
4920 return ["def (not an object)"];
4921 }
4922 const d = def;
4923 const errors = [];
4924 for (const check of checks) {
4925 if (!check.valid(d)) {
4926 errors.push(`${check.field} (${check.message})`);
4927 }
4928 }
4929 return errors;
4930 }
4931 class RegistrationError extends Error {
4932 constructor(kind, errors, def) {
4933 super(
4934 `[desktop-mode] ${kind} registration rejected — fields: ` + errors.join(", ") + "."
4935 );
4936 this.name = "RegistrationError";
4937 this.kind = kind;
4938 this.errors = errors;
4939 this.def = def;
4940 }
4941 }
4942 function throwOnRegistrationErrors(kind, errors, def) {
4943 if (errors.length === 0) {
4944 return;
4945 }
4946 throw new RegistrationError(kind, errors, def);
4947 }
4948 function logRegistrationErrors(kind, errors, def) {
4949 if (typeof console === "undefined") {
4950 return;
4951 }
4952 console.warn(
4953 `[desktop-mode] ${kind} registration rejected — fields: ` + errors.join(", ") + ".",
4954 def
4955 );
4956 }
4957 const MAX_LINKS = 32;
4958 const MAX_RELATED = 64;
4959 const CONTENT_TYPE_ID = /^[a-z0-9_/-]+$/;
4960 const store$i = createSharedStore(
4961 "desktop-mode/window-links",
4962 () => ({
4963 contentByWindow: /* @__PURE__ */ new Map(),
4964 focusSeq: /* @__PURE__ */ new Map(),
4965 seq: 0,
4966 listeners: /* @__PURE__ */ new Set(),
4967 lastGroupsSignature: "",
4968 manager: null,
4969 started: false
4970 })
4971 );
4972 function keyOf(ref) {
4973 return `${ref.type}:${ref.id}`;
4974 }
4975 function rootKeyOf(ref) {
4976 return ref.root ? keyOf(ref.root) : keyOf(ref);
4977 }
4978 function validateRef(ref) {
4979 const isValidId = (v) => typeof v === "number" && Number.isFinite(v) || typeof v === "string" && v.trim() !== "";
4980 const isValidType = (v) => typeof v === "string" && CONTENT_TYPE_ID.test(v.trim().toLowerCase());
4981 return collectRegistrationErrors(ref, [
4982 {
4983 field: "type",
4984 valid: (r) => isValidType(r.type),
4985 message: "must match /^[a-z0-9_/-]+$/ — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-type"
4986 },
4987 {
4988 field: "id",
4989 valid: (r) => isValidId(r.id),
4990 message: "must be a finite number or non-empty string"
4991 },
4992 {
4993 field: "root",
4994 valid: (r) => r.root === void 0 || !!r.root && typeof r.root === "object" && isValidType(r.root.type) && isValidId(r.root.id),
4995 message: "when present, must be { type, id } with the same shapes as the ref itself"
4996 },
4997 {
4998 field: "related",
4999 valid: (r) => r.related === void 0 || Array.isArray(r.related) && r.related.every(
5000 (item) => !!item && typeof item === "object" && typeof item.id === "string" && item.id.trim() !== "" && typeof item.group === "string" && item.group.trim() !== "" && typeof item.label === "string" && item.label.trim() !== "" && typeof item.url === "string" && item.url.trim() !== "" && (item.groupLabel === void 0 || typeof item.groupLabel === "string") && (item.icon === void 0 || typeof item.icon === "string") && (item.count === void 0 || typeof item.count === "number" && Number.isFinite(item.count))
5001 ),
5002 message: "when present, must be an array of { id, group, label, url, groupLabel?, icon?, count? } entries with non-empty strings"
5003 },
5004 {
5005 field: "links",
5006 valid: (r) => r.links === void 0 || Array.isArray(r.links) && r.links.every(
5007 (l) => !!l && typeof l === "object" && isValidType(l.type) && isValidId(l.id) && (l.rel === void 0 || l.rel === "references" || l.rel === "child")
5008 ),
5009 message: "when present, must be an array of { type, id, rel?: 'references'|'child' } entries"
5010 }
5011 ]);
5012 }
5013 function normalizeRef(ref, source) {
5014 const next = {
5015 type: ref.type.trim().toLowerCase(),
5016 id: ref.id,
5017 source
5018 };
5019 if (ref.root) {
5020 next.root = {
5021 type: ref.root.type.trim().toLowerCase(),
5022 id: ref.root.id
5023 };
5024 }
5025 if (Array.isArray(ref.links) && ref.links.length > 0) {
5026 next.links = ref.links.slice(0, MAX_LINKS).map((l) => {
5027 const entry = {
5028 type: l.type.trim().toLowerCase(),
5029 id: l.id
5030 };
5031 if (l.rel === "child") {
5032 entry.rel = "child";
5033 }
5034 return entry;
5035 });
5036 }
5037 if (typeof ref.label === "string" && ref.label !== "") {
5038 next.label = ref.label;
5039 }
5040 if (Array.isArray(ref.related) && ref.related.length > 0) {
5041 next.related = ref.related.slice(0, MAX_RELATED).map((item) => {
5042 const entry = {
5043 id: item.id,
5044 group: item.group,
5045 label: item.label,
5046 url: item.url
5047 };
5048 if (typeof item.groupLabel === "string" && item.groupLabel !== "") {
5049 entry.groupLabel = item.groupLabel;
5050 }
5051 if (typeof item.icon === "string" && item.icon !== "") {
5052 entry.icon = item.icon;
5053 }
5054 if (typeof item.count === "number") {
5055 entry.count = item.count;
5056 }
5057 return entry;
5058 });
5059 }
5060 return next;
5061 }
5062 function relatedSignature(ref) {
5063 if (!ref || !ref.related) {
5064 return "";
5065 }
5066 return ref.related.map(
5067 (item) => `${item.id}\0${item.group}\0${item.label}\0${item.url}\0${item.groupLabel ?? ""}\0${item.icon ?? ""}\0${item.count ?? ""}`
5068 ).join("|");
5069 }
5070 function refSignature(ref) {
5071 if (!ref) {
5072 return "";
5073 }
5074 return [
5075 keyOf(ref),
5076 rootKeyOf(ref),
5077 ...(ref.links ?? []).map(
5078 (l) => keyOf(l) + (l.rel === "child" ? "!child" : "")
5079 )
5080 ].join("|");
5081 }
5082 function setWindowContent(windowId, ref, opts = {}) {
5083 const source = opts.source ?? "api";
5084 if (typeof windowId !== "string" || windowId === "") {
5085 throwOnRegistrationErrors(
5086 "WindowContentRef",
5087 ["windowId (must be a non-empty string)"],
5088 ref
5089 );
5090 return;
5091 }
5092 let next = null;
5093 if (ref !== null && ref !== void 0) {
5094 const errors = validateRef(ref);
5095 if (errors.length > 0) {
5096 if (source === "api") {
5097 throwOnRegistrationErrors("WindowContentRef", errors, ref);
5098 }
5099 logRegistrationErrors("WindowContentRef", errors, ref);
5100 return;
5101 }
5102 next = normalizeRef(ref, source);
5103 }
5104 next = applyFilters(
5105 HOOKS.WINDOW_LINKS_CONTENT,
5106 next,
5107 { windowId, source }
5108 );
5109 if (next !== null && (!next || validateRef(next).length > 0)) {
5110 logRegistrationErrors(
5111 "WindowContentRef",
5112 ["filter (desktop-mode.window-links.content returned an invalid ref)"],
5113 next
5114 );
5115 return;
5116 }
5117 const previous = store$i.state.contentByWindow.get(windowId) ?? null;
5118 if (next === null && previous === null) {
5119 return;
5120 }
5121 if (next !== null && previous !== null && refSignature(next) === refSignature(previous) && next.label === previous.label && relatedSignature(next) === relatedSignature(previous)) {
5122 return;
5123 }
5124 if (next === null) {
5125 store$i.state.contentByWindow.delete(windowId);
5126 } else {
5127 store$i.state.contentByWindow.set(windowId, next);
5128 }
5129 const changedDetail = { windowId, content: next, previous, source };
5130 document.dispatchEvent(
5131 new CustomEvent("desktop-mode-window-content-changed", {
5132 detail: changedDetail
5133 })
5134 );
5135 doAction(HOOKS.WINDOW_CONTENT_CHANGED, changedDetail);
5136 broadcastGroupsIfChanged();
5137 notify$i();
5138 }
5139 function getWindowContent(windowId) {
5140 return store$i.state.contentByWindow.get(windowId);
5141 }
5142 function listWindowLinkGroups() {
5143 const byKey = /* @__PURE__ */ new Map();
5144 for (const [windowId, ref] of store$i.state.contentByWindow) {
5145 const groupKey = rootKeyOf(ref);
5146 let group = byKey.get(groupKey);
5147 if (!group) {
5148 group = {
5149 key: groupKey,
5150 root: ref.root ? { ...ref.root } : { type: ref.type, id: ref.id },
5151 rootWindowIds: [],
5152 children: []
5153 };
5154 byKey.set(groupKey, group);
5155 }
5156 if (ref.root) {
5157 group.children.push({ windowId, content: ref });
5158 } else {
5159 group.rootWindowIds.push(windowId);
5160 }
5161 }
5162 const seq = store$i.state.focusSeq;
5163 for (const group of byKey.values()) {
5164 group.rootWindowIds.sort(
5165 (a, b) => (seq.get(b) ?? 0) - (seq.get(a) ?? 0)
5166 );
5167 }
5168 const copy = Array.from(byKey.values());
5169 const filtered = applyFilters(
5170 HOOKS.WINDOW_LINK_GROUPS,
5171 copy
5172 );
5173 if (!Array.isArray(filtered)) {
5174 if (typeof console !== "undefined") {
5175 console.warn(
5176 "[desktop-mode] `desktop-mode.window-links.groups` filter returned a non-array; falling back to computed groups."
5177 );
5178 }
5179 return copy;
5180 }
5181 return filtered;
5182 }
5183 function getWindowLinkGroup(windowId) {
5184 return listWindowLinkGroups().find(
5185 (g) => g.rootWindowIds.includes(windowId) || g.children.some((c) => c.windowId === windowId)
5186 );
5187 }
5188 function getRelatedWindowIds(windowId) {
5189 const related = /* @__PURE__ */ new Set();
5190 const group = getWindowLinkGroup(windowId);
5191 if (group) {
5192 for (const id of [
5193 ...group.rootWindowIds,
5194 ...group.children.map((c) => c.windowId)
5195 ]) {
5196 related.add(id);
5197 }
5198 }
5199 for (const edge of listWindowLinkEdges()) {
5200 if (edge.fromWindowId === windowId) {
5201 related.add(edge.toWindowId);
5202 } else if (edge.toWindowId === windowId) {
5203 related.add(edge.fromWindowId);
5204 }
5205 }
5206 related.delete(windowId);
5207 return Array.from(related);
5208 }
5209 function getDirectlyRelatedWindowIds(windowId) {
5210 const related = /* @__PURE__ */ new Set();
5211 for (const edge of listWindowLinkEdges()) {
5212 if (edge.fromWindowId === windowId) {
5213 related.add(edge.toWindowId);
5214 } else if (edge.toWindowId === windowId) {
5215 related.add(edge.fromWindowId);
5216 }
5217 }
5218 related.delete(windowId);
5219 return Array.from(related);
5220 }
5221 function listWindowLinkEdges() {
5222 const seq = store$i.state.focusSeq;
5223 const windowByKey = /* @__PURE__ */ new Map();
5224 for (const [windowId, ref] of store$i.state.contentByWindow) {
5225 const key = keyOf(ref);
5226 const current = windowByKey.get(key);
5227 if (!current || (seq.get(windowId) ?? 0) > (seq.get(current) ?? 0)) {
5228 windowByKey.set(key, windowId);
5229 }
5230 }
5231 const edges = /* @__PURE__ */ new Map();
5232 const directedKey = (from, to) => `${from}→${to}`;
5233 for (const [windowId, ref] of store$i.state.contentByWindow) {
5234 if (ref.root) {
5235 const target2 = windowByKey.get(keyOf(ref.root));
5236 if (target2 && target2 !== windowId) {
5237 edges.set(directedKey(windowId, target2), {
5238 fromWindowId: windowId,
5239 toWindowId: target2,
5240 kind: "child-root",
5241 bidirectional: false
5242 });
5243 }
5244 }
5245 for (const link of ref.links ?? []) {
5246 const target2 = windowByKey.get(keyOf(link));
5247 if (!target2 || target2 === windowId) {
5248 continue;
5249 }
5250 if (link.rel === "child") {
5251 const key2 = directedKey(target2, windowId);
5252 const existing = edges.get(key2);
5253 if (!existing || existing.kind !== "child-root") {
5254 edges.set(key2, {
5255 fromWindowId: target2,
5256 toWindowId: windowId,
5257 kind: "child-root",
5258 bidirectional: false
5259 });
5260 }
5261 continue;
5262 }
5263 const key = directedKey(windowId, target2);
5264 if (!edges.has(key)) {
5265 edges.set(key, {
5266 fromWindowId: windowId,
5267 toWindowId: target2,
5268 kind: "reference",
5269 bidirectional: false
5270 });
5271 }
5272 }
5273 }
5274 const merged = [];
5275 const dropped = /* @__PURE__ */ new Set();
5276 for (const [key, edge] of edges) {
5277 if (dropped.has(key)) {
5278 continue;
5279 }
5280 const reverseKey = directedKey(edge.toWindowId, edge.fromWindowId);
5281 const reverse = edges.get(reverseKey);
5282 if (reverse && edge.kind === "reference") {
5283 if (reverse.kind === "reference") {
5284 dropped.add(reverseKey);
5285 merged.push({ ...edge, bidirectional: true });
5286 continue;
5287 }
5288 continue;
5289 }
5290 merged.push(edge);
5291 }
5292 const filtered = applyFilters(
5293 HOOKS.WINDOW_LINK_EDGES,
5294 merged
5295 );
5296 if (!Array.isArray(filtered)) {
5297 if (typeof console !== "undefined") {
5298 console.warn(
5299 "[desktop-mode] `desktop-mode.window-links.edges` filter returned a non-array; falling back to derived edges."
5300 );
5301 }
5302 return merged;
5303 }
5304 return filtered;
5305 }
5306 function subscribeWindowLinks(cb) {
5307 store$i.state.listeners.add(cb);
5308 return () => {
5309 store$i.state.listeners.delete(cb);
5310 };
5311 }
5312 function notify$i() {
5313 for (const cb of Array.from(store$i.state.listeners)) {
5314 try {
5315 cb();
5316 } catch (err) {
5317 if (typeof console !== "undefined") {
5318 console.error(
5319 "[desktop-mode] window-links listener threw:",
5320 err
5321 );
5322 }
5323 }
5324 }
5325 }
5326 function relationsSignature() {
5327 return Array.from(store$i.state.contentByWindow).map(([id, ref]) => `${id}=${refSignature(ref)}`).sort().join(";");
5328 }
5329 function broadcastGroupsIfChanged() {
5330 const signature = relationsSignature();
5331 if (signature === store$i.state.lastGroupsSignature) {
5332 return;
5333 }
5334 store$i.state.lastGroupsSignature = signature;
5335 const groups = listWindowLinkGroups();
5336 const detail = { groups };
5337 document.dispatchEvent(
5338 new CustomEvent("desktop-mode-window-link-groups-changed", {
5339 detail
5340 })
5341 );
5342 doAction(HOOKS.WINDOW_LINK_GROUPS_CHANGED, detail);
5343 }
5344 const relationsApi = {
5345 get: getWindowContent,
5346 set: (windowId, ref) => setWindowContent(windowId, ref, { source: "api" }),
5347 groups: listWindowLinkGroups,
5348 edges: listWindowLinkEdges,
5349 groupOf: getWindowLinkGroup,
5350 related: getRelatedWindowIds,
5351 subscribe: subscribeWindowLinks
5352 };
5353 function startWindowLinksEngine({
5354 manager: manager2
5355 }) {
5356 store$i.state.manager = manager2;
5357 if (store$i.state.started) {
5358 return;
5359 }
5360 store$i.state.started = true;
5361 addAction(
5362 HOOKS.WINDOW_OPENED,
5363 "desktop-mode/window-links-seed",
5364 (e) => {
5365 if (!e?.windowId) {
5366 return;
5367 }
5368 const win = store$i.state.manager?.getById(e.windowId);
5369 const content = win?.config?.content;
5370 if (content) {
5371 setWindowContent(e.windowId, content, { source: "config" });
5372 }
5373 }
5374 );
5375 addAction(
5376 HOOKS.WINDOW_CLOSED,
5377 "desktop-mode/window-links-clear",
5378 (e) => {
5379 if (!e?.windowId) {
5380 return;
5381 }
5382 store$i.state.focusSeq.delete(e.windowId);
5383 setWindowContent(e.windowId, null, { source: "config" });
5384 }
5385 );
5386 addAction(
5387 HOOKS.WINDOW_FOCUSED,
5388 "desktop-mode/window-links-recency",
5389 (e) => {
5390 if (!e?.windowId) {
5391 return;
5392 }
5393 store$i.state.seq += 1;
5394 store$i.state.focusSeq.set(e.windowId, store$i.state.seq);
5395 }
5396 );
5397 window.addEventListener("message", (event) => {
5398 if (event.origin !== window.location.origin) {
5399 return;
5400 }
5401 const data = event.data;
5402 if (!data || data.type !== "desktop-mode-content-identity") {
5403 return;
5404 }
5405 const win = store$i.state.manager?.findByIframeSource?.(
5406 event.source
5407 );
5408 if (!win) {
5409 return;
5410 }
5411 setWindowContent(win.id, data.identity ?? null, {
5412 source: "bridge"
5413 });
5414 });
5415 }
5416 const adminLinkDepsStore = createSharedStore(
5417 "desktop-mode/admin-link-deps",
5418 () => ({ deps: null })
5419 );
5420 function bindAdminLinkDispatch(deps2) {
5421 adminLinkDepsStore.state.deps = deps2;
5422 }
5423 const store$h = createSharedStore(
5424 "desktop-mode/wallpaper-registry",
5425 () => ({
5426 seed: [],
5427 listeners: /* @__PURE__ */ new Set()
5428 })
5429 );
5430 const seed$4 = store$h.state.seed;
5431 const listeners$e = store$h.state.listeners;
5432 function register$3(def) {
5433 throwOnRegistrationErrors(
5434 "Wallpaper",
5435 collectRegistrationErrors(def, WALLPAPER_CHECKS),
5436 def
5437 );
5438 const idx = seed$4.findIndex((w) => w.id === def.id);
5439 if (idx >= 0) {
5440 seed$4[idx] = def;
5441 } else {
5442 seed$4.push(def);
5443 }
5444 notify$h();
5445 }
5446 function unregister$3(id) {
5447 const idx = seed$4.findIndex((w) => w.id === id);
5448 if (idx >= 0) {
5449 seed$4.splice(idx, 1);
5450 notify$h();
5451 }
5452 }
5453 function notify$h() {
5454 const snapshot = Array.from(listeners$e);
5455 for (const cb of snapshot) {
5456 try {
5457 cb();
5458 } catch (err) {
5459 if (typeof console !== "undefined") {
5460 console.error(
5461 "[desktop-mode] wallpaper registry listener threw:",
5462 err
5463 );
5464 }
5465 }
5466 }
5467 }
5468 function all$2() {
5469 const copy = seed$4.slice();
5470 const filtered = applyFilters(HOOKS.WALLPAPERS, copy);
5471 if (!Array.isArray(filtered)) {
5472 if (typeof console !== "undefined") {
5473 console.warn(
5474 "[desktop-mode] `desktop-mode.wallpapers` filter returned a non-array; falling back to seed list."
5475 );
5476 }
5477 return copy;
5478 }
5479 return filtered.filter(isValidDef$1);
5480 }
5481 function get$2(id) {
5482 return all$2().find((w) => w.id === id);
5483 }
5484 const WALLPAPER_CHECKS = [
5485 {
5486 field: "id",
5487 message: "missing or not a non-empty string",
5488 valid: (d) => typeof d.id === "string" && d.id !== ""
5489 },
5490 {
5491 field: "label",
5492 message: "missing or not a non-empty string",
5493 valid: (d) => typeof d.label === "string" && d.label !== ""
5494 },
5495 {
5496 field: "preview",
5497 message: "missing or not a non-empty string",
5498 valid: (d) => typeof d.preview === "string" && d.preview !== ""
5499 },
5500 {
5501 field: "type",
5502 message: 'must be "css" or "canvas"',
5503 valid: (d) => d.type === "css" || d.type === "canvas"
5504 },
5505 {
5506 field: "value/resolveValue/mount",
5507 message: "css types need `value` or `resolveValue`; canvas types need `mount`",
5508 valid: (d) => {
5509 if (d.type === "css") {
5510 return typeof d.value === "string" || typeof d.resolveValue === "function";
5511 }
5512 if (d.type === "canvas") {
5513 return typeof d.mount === "function";
5514 }
5515 return true;
5516 }
5517 }
5518 ];
5519 function isValidDef$1(def) {
5520 return collectRegistrationErrors(def, WALLPAPER_CHECKS).length === 0;
5521 }
5522 const store$g = createSharedStore(
5523 "desktop-mode/wallpaper-settings",
5524 () => ({ values: {} })
5525 );
5526 function getWallpaperSettings(id) {
5527 return { ...store$g.state.values[id] ?? {} };
5528 }
5529 function seedWallpaperSettings(all2) {
5530 const values = store$g.state.values;
5531 for (const key of Object.keys(values)) {
5532 delete values[key];
5533 }
5534 for (const [id, settings] of Object.entries(all2)) {
5535 values[id] = { ...settings };
5536 }
5537 }
5538 const STORAGE_KEY = "desktop-mode-os-settings";
5539 const CUSTOM_GRADIENT_ID = "custom-gradient";
5540 const CUSTOM_IMAGE_ID = "custom-image";
5541 const DEFAULT_WALLPAPER_ID = "dark";
5542 const DEFAULT_ACCENTS = [
5543 { id: "wp-blue", label: "WordPress Blue", value: "#2271b1" },
5544 { id: "indigo", label: "Indigo", value: "#3858e9" },
5545 { id: "teal", label: "Teal", value: "#04a4cc" },
5546 { id: "emerald", label: "Emerald", value: "#059669" },
5547 { id: "amber", label: "Amber", value: "#d97706" },
5548 { id: "rose", label: "Rose", value: "#e11d48" }
5549 ];
5550 function getAccents() {
5551 const config = window.wp?.desktop?.config;
5552 const raw = config?.accentColors;
5553 if (!Array.isArray(raw) || raw.length === 0) {
5554 return DEFAULT_ACCENTS;
5555 }
5556 const clean = [];
5557 for (const entry of raw) {
5558 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)) {
5559 clean.push({ id: entry.id, label: entry.label, value: entry.value });
5560 }
5561 }
5562 return clean.length > 0 ? clean : DEFAULT_ACCENTS;
5563 }
5564 function getDefaultWallpaperId() {
5565 const config = window.wp?.desktop?.config;
5566 const raw = config?.defaultWallpaper;
5567 if (typeof raw === "string" && raw !== "") {
5568 return raw;
5569 }
5570 return DEFAULT_WALLPAPER_ID;
5571 }
5572 const DOCK_SIZES = [
5573 { id: "compact", label: "Compact", width: 48, icon: 18 },
5574 { id: "default", label: "Default", width: 56, icon: 20 },
5575 { id: "large", label: "Large", width: 72, icon: 26 }
5576 ];
5577 const DESKTOP_LAYOUTS = [
5578 { id: "classic", label: "Classic" },
5579 { id: "unified", label: "Unified" },
5580 { id: "spatial", label: "Spatial" }
5581 ];
5582 const DEFAULTS = {
5583 wallpaper: DEFAULT_WALLPAPER_ID,
5584 accent: "wp-blue",
5585 dockSize: "default",
5586 desktopLayout: "classic",
5587 dockRailRenderer: "default",
5588 unfocusEffect: "darken",
5589 windowLinkRenderer: "svg-splines",
5590 windowLinkVisibility: "always",
5591 windowLinksEnabled: true,
5592 windowLinkRaiseOnFocus: true,
5593 windowLinkHighlight: true,
5594 customGradient: {
5595 from: "#2271b1",
5596 to: "#7c3aed",
5597 angle: 135
5598 },
5599 customImage: null,
5600 wallpaperSettings: {},
5601 libraryHdOnly: true,
5602 ai: {
5603 enabled: false
5604 },
5605 // Opt-IN Beta as of 0.9.1. Fresh installs land on the classic
5606 // chromeless `edit.php` iframe; a user opts in via OS Settings →
5607 // Features → Beta features to get the native Posts window. The
5608 // native windows used to default ON (opt-out, 0.8.0) but are now
5609 // opt-in so the redesign is a deliberate choice, not imposed.
5610 heartbeatRate: 60,
5611 nativePostsEnabled: false,
5612 nativePostsHiddenColumns: [],
5613 // Same opt-in Beta posture as Posts — fresh installs keep the
5614 // iframe; users opt in to the native Pages window.
5615 nativePagesEnabled: false,
5616 // Native Users window — same opt-in Beta posture. Capability-gated
5617 // server-side (the window is only registered for users with
5618 // `list_users`), so this toggle only affects the small set of
5619 // users who can see the Users tile in the first place.
5620 nativeUsersEnabled: false,
5621 // Native Plugins window — replaces `plugins.php` and
5622 // `plugin-install.php`. Same opt-in Beta posture; cap-gated on
5623 // `activate_plugins` server-side, so this toggle only affects
5624 // users who could see the Plugins tile anyway.
5625 nativePluginsEnabled: false,
5626 // Native Comments window — replaces `edit-comments.php`. Same
5627 // opt-in Beta posture; cap-gated on `edit_posts` server-side.
5628 nativeCommentsEnabled: false,
5629 showDesktopOnWallpaperClick: false,
5630 showPostStatusRibbons: true,
5631 developerModeEnabled: false,
5632 foldersSharingEnabled: true,
5633 itemVisibility: {},
5634 dockOrder: [],
5635 dockPromotedPositions: {}
5636 };
5637 function isHexColor(value) {
5638 return typeof value === "string" && /^#[0-9a-f]{3,8}$/i.test(value);
5639 }
5640 const NONCE_HEADER = "X-WP-Nonce";
5641 function injectRestNonce(input, init2) {
5642 const nonce = readRestNonce$3();
5643 if (!nonce) {
5644 return init2;
5645 }
5646 const url = resolveUrl(input);
5647 if (!url || !isSameOriginRestUrl(url)) {
5648 return init2;
5649 }
5650 const baseHeaders = init2?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
5651 const headers = new Headers(baseHeaders ?? {});
5652 if (headers.has(NONCE_HEADER)) {
5653 return init2;
5654 }
5655 headers.set(NONCE_HEADER, nonce);
5656 return { ...init2 ?? {}, headers };
5657 }
5658 function readRestNonce$3() {
5659 if (typeof window === "undefined") {
5660 return void 0;
5661 }
5662 const cfg = window.desktopModeConfig;
5663 const value = cfg?.restNonce;
5664 return typeof value === "string" && value.length > 0 ? value : void 0;
5665 }
5666 function resolveUrl(input) {
5667 try {
5668 const base = typeof window !== "undefined" && window.location ? window.location.href : void 0;
5669 if (typeof input === "string") {
5670 return new URL(input, base);
5671 }
5672 if (input instanceof URL) {
5673 return input;
5674 }
5675 if (typeof Request !== "undefined" && input instanceof Request) {
5676 return new URL(input.url, base);
5677 }
5678 return null;
5679 } catch {
5680 return null;
5681 }
5682 }
5683 function isSameOriginRestUrl(url) {
5684 if (typeof window === "undefined" || !window.location || url.origin !== window.location.origin) {
5685 return false;
5686 }
5687 if (url.pathname.includes("/wp-json/")) {
5688 return true;
5689 }
5690 if (url.searchParams.has("rest_route")) {
5691 return true;
5692 }
5693 return false;
5694 }
5695 function trackedFetch$1(input, init2, opts = {}) {
5696 const fn = window.wp?.desktop?.fetch;
5697 if (typeof fn === "function") {
5698 return fn(input, init2, opts);
5699 }
5700 const finalInit = injectRestNonce(input, init2);
5701 return fetch(input, finalInit);
5702 }
5703 function loadState() {
5704 const serverRaw = _readServerSettings();
5705 if (serverRaw) {
5706 const state2 = _parseRaw(serverRaw);
5707 _writeLocalStorage(state2);
5708 return state2;
5709 }
5710 try {
5711 const cached = window.localStorage.getItem(STORAGE_KEY);
5712 if (cached) {
5713 return _parseRaw(JSON.parse(cached));
5714 }
5715 } catch {
5716 }
5717 return structuredDefaults();
5718 }
5719 function _readServerSettings() {
5720 const config = window.desktopModeConfig;
5721 const raw = config?.osSettings;
5722 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5723 return null;
5724 }
5725 return raw;
5726 }
5727 function _parseRaw(parsed) {
5728 const accents = getAccents();
5729 return {
5730 wallpaper: typeof parsed.wallpaper === "string" && parsed.wallpaper !== "" ? parsed.wallpaper : getDefaultWallpaperId(),
5731 accent: accents.some((a) => a.id === parsed.accent) ? parsed.accent : DEFAULTS.accent,
5732 dockSize: DOCK_SIZES.some((d) => d.id === parsed.dockSize) ? parsed.dockSize : DEFAULTS.dockSize,
5733 desktopLayout: DESKTOP_LAYOUTS.some(
5734 (l) => l.id === parsed.desktopLayout
5735 ) ? parsed.desktopLayout : DEFAULTS.desktopLayout,
5736 // Dock rail renderer — any sanitize_key()-clean string
5737 // survives; the registry resolves at use time and falls back
5738 // to `'default'` when the picked renderer isn't registered.
5739 dockRailRenderer: typeof parsed.dockRailRenderer === "string" && /^[a-z0-9_-]+$/.test(parsed.dockRailRenderer) ? parsed.dockRailRenderer : DEFAULTS.dockRailRenderer,
5740 // Unfocus effect — any registry id (`vendor/sub-id` allowed) or
5741 // the `'none'` sentinel survives; the engine resolves at use
5742 // time and treats an unknown id as "no effect".
5743 unfocusEffect: typeof parsed.unfocusEffect === "string" && /^[a-z0-9_/-]+$/.test(parsed.unfocusEffect) ? parsed.unfocusEffect : DEFAULTS.unfocusEffect,
5744 // Window-link renderer — same id charset as unfocus effects;
5745 // the render host resolves at use time and falls back to the
5746 // built-in `svg-splines` for unknown ids.
5747 windowLinkRenderer: typeof parsed.windowLinkRenderer === "string" && /^[a-z0-9_/-]+$/.test(parsed.windowLinkRenderer) ? parsed.windowLinkRenderer : DEFAULTS.windowLinkRenderer,
5748 windowLinkVisibility: parsed.windowLinkVisibility === "focus" || parsed.windowLinkVisibility === "always" || parsed.windowLinkVisibility === "off" ? parsed.windowLinkVisibility : DEFAULTS.windowLinkVisibility,
5749 windowLinksEnabled: typeof parsed.windowLinksEnabled === "boolean" ? parsed.windowLinksEnabled : DEFAULTS.windowLinksEnabled,
5750 windowLinkRaiseOnFocus: typeof parsed.windowLinkRaiseOnFocus === "boolean" ? parsed.windowLinkRaiseOnFocus : DEFAULTS.windowLinkRaiseOnFocus,
5751 windowLinkHighlight: typeof parsed.windowLinkHighlight === "boolean" ? parsed.windowLinkHighlight : DEFAULTS.windowLinkHighlight,
5752 customGradient: sanitizeCustomGradient(parsed.customGradient),
5753 customImage: sanitizeCustomImage(parsed.customImage),
5754 wallpaperSettings: sanitizeWallpaperSettings(parsed.wallpaperSettings),
5755 libraryHdOnly: typeof parsed.libraryHdOnly === "boolean" ? parsed.libraryHdOnly : DEFAULTS.libraryHdOnly,
5756 ai: sanitizeAi(parsed.ai),
5757 heartbeatRate: parsed.heartbeatRate === 15 || parsed.heartbeatRate === 30 || parsed.heartbeatRate === 45 || parsed.heartbeatRate === 60 ? parsed.heartbeatRate : DEFAULTS.heartbeatRate,
5758 nativePostsEnabled: typeof parsed.nativePostsEnabled === "boolean" ? parsed.nativePostsEnabled : DEFAULTS.nativePostsEnabled,
5759 nativePostsHiddenColumns: Array.isArray(parsed.nativePostsHiddenColumns) ? parsed.nativePostsHiddenColumns.filter((v) => typeof v === "string" && v !== "").slice(0, 32) : DEFAULTS.nativePostsHiddenColumns.slice(),
5760 nativePagesEnabled: typeof parsed.nativePagesEnabled === "boolean" ? parsed.nativePagesEnabled : DEFAULTS.nativePagesEnabled,
5761 nativeUsersEnabled: typeof parsed.nativeUsersEnabled === "boolean" ? parsed.nativeUsersEnabled : DEFAULTS.nativeUsersEnabled,
5762 nativePluginsEnabled: typeof parsed.nativePluginsEnabled === "boolean" ? parsed.nativePluginsEnabled : DEFAULTS.nativePluginsEnabled,
5763 nativeCommentsEnabled: typeof parsed.nativeCommentsEnabled === "boolean" ? parsed.nativeCommentsEnabled : DEFAULTS.nativeCommentsEnabled,
5764 showDesktopOnWallpaperClick: typeof parsed.showDesktopOnWallpaperClick === "boolean" ? parsed.showDesktopOnWallpaperClick : DEFAULTS.showDesktopOnWallpaperClick,
5765 showPostStatusRibbons: typeof parsed.showPostStatusRibbons === "boolean" ? parsed.showPostStatusRibbons : DEFAULTS.showPostStatusRibbons,
5766 developerModeEnabled: typeof parsed.developerModeEnabled === "boolean" ? parsed.developerModeEnabled : DEFAULTS.developerModeEnabled,
5767 foldersSharingEnabled: typeof parsed.foldersSharingEnabled === "boolean" ? parsed.foldersSharingEnabled : DEFAULTS.foldersSharingEnabled,
5768 itemVisibility: sanitizeItemVisibility(parsed.itemVisibility),
5769 dockOrder: sanitizeDockOrder(parsed.dockOrder),
5770 dockPromotedPositions: sanitizeDockPromotedPositions(
5771 parsed.dockPromotedPositions
5772 )
5773 };
5774 }
5775 function sanitizeWallpaperSettings(raw) {
5776 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5777 return {};
5778 }
5779 const out = {};
5780 let idCount = 0;
5781 for (const [id, bag] of Object.entries(
5782 raw
5783 )) {
5784 if (idCount >= 64) {
5785 break;
5786 }
5787 if (typeof id !== "string" || id === "" || !/^[a-z0-9_/-]+$/.test(id)) {
5788 continue;
5789 }
5790 if (!bag || typeof bag !== "object" || Array.isArray(bag)) {
5791 continue;
5792 }
5793 const clean = {};
5794 let keyCount = 0;
5795 for (const [key, value] of Object.entries(
5796 bag
5797 )) {
5798 if (keyCount >= 32) {
5799 break;
5800 }
5801 if (typeof key !== "string" || key === "" || !/^[a-zA-Z0-9_-]+$/.test(key)) {
5802 continue;
5803 }
5804 if (typeof value === "boolean") {
5805 clean[key] = value;
5806 } else if (typeof value === "number" && Number.isFinite(value)) {
5807 clean[key] = value;
5808 } else if (typeof value === "string") {
5809 clean[key] = value.slice(0, 256);
5810 } else {
5811 continue;
5812 }
5813 keyCount++;
5814 }
5815 if (keyCount === 0) {
5816 continue;
5817 }
5818 out[id] = clean;
5819 idCount++;
5820 }
5821 return out;
5822 }
5823 function sanitizeItemVisibility(raw) {
5824 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5825 return {};
5826 }
5827 const allowed = [
5828 "both",
5829 "dock",
5830 "desktop",
5831 "hidden"
5832 ];
5833 const out = {};
5834 let count = 0;
5835 for (const [k, v] of Object.entries(raw)) {
5836 if (count >= 256) {
5837 break;
5838 }
5839 if (typeof k !== "string" || k === "") {
5840 continue;
5841 }
5842 if (typeof v !== "string") {
5843 continue;
5844 }
5845 const placement = v;
5846 if (!allowed.includes(placement)) {
5847 continue;
5848 }
5849 out[k] = placement;
5850 count++;
5851 }
5852 return out;
5853 }
5854 function sanitizeDockOrder(raw) {
5855 if (!Array.isArray(raw)) {
5856 return [];
5857 }
5858 const out = [];
5859 const seen = /* @__PURE__ */ new Set();
5860 for (const id of raw) {
5861 if (typeof id !== "string" || id === "" || seen.has(id)) {
5862 continue;
5863 }
5864 seen.add(id);
5865 out.push(id);
5866 if (out.length >= 256) {
5867 break;
5868 }
5869 }
5870 return out;
5871 }
5872 function sanitizeDockPromotedPositions(raw) {
5873 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5874 return {};
5875 }
5876 const out = {};
5877 let count = 0;
5878 const MAX_COORD = 1e5;
5879 for (const [k, v] of Object.entries(raw)) {
5880 if (count >= 256) {
5881 break;
5882 }
5883 if (typeof k !== "string" || k === "") {
5884 continue;
5885 }
5886 if (!v || typeof v !== "object" || Array.isArray(v)) {
5887 continue;
5888 }
5889 const pos = v;
5890 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) {
5891 continue;
5892 }
5893 out[k] = { x: pos.x, y: pos.y };
5894 count++;
5895 }
5896 return out;
5897 }
5898 let _syncTimer = null;
5899 const SYNC_DEBOUNCE_MS = 250;
5900 let _lastConfirmedState = null;
5901 function setLastConfirmedState(state2) {
5902 _lastConfirmedState = _cloneState(state2);
5903 }
5904 function _cloneState(state2) {
5905 return {
5906 ...state2,
5907 customGradient: { ...state2.customGradient },
5908 customImage: state2.customImage ? { ...state2.customImage } : null,
5909 wallpaperSettings: Object.fromEntries(
5910 Object.entries(state2.wallpaperSettings).map(([k, v]) => [
5911 k,
5912 { ...v }
5913 ])
5914 ),
5915 ai: { ...state2.ai },
5916 nativePostsHiddenColumns: state2.nativePostsHiddenColumns.slice(),
5917 itemVisibility: { ...state2.itemVisibility },
5918 dockOrder: state2.dockOrder.slice(),
5919 dockPromotedPositions: Object.fromEntries(
5920 Object.entries(state2.dockPromotedPositions).map(([k, v]) => [
5921 k,
5922 { ...v }
5923 ])
5924 )
5925 };
5926 }
5927 function saveState(state2, opts = {}) {
5928 _writeLocalStorage(state2);
5929 _scheduleSyncToServer(state2, opts.windowId);
5930 }
5931 function _writeLocalStorage(state2) {
5932 try {
5933 window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state2));
5934 } catch {
5935 }
5936 }
5937 function _scheduleSyncToServer(state2, windowId) {
5938 if (_syncTimer !== null) {
5939 clearTimeout(_syncTimer);
5940 }
5941 if (windowId) {
5942 _pendingActivityWindowId = windowId;
5943 }
5944 _emitSaveLifecycle("pending");
5945 _syncTimer = setTimeout(() => {
5946 _syncTimer = null;
5947 const id = _pendingActivityWindowId;
5948 _pendingActivityWindowId = null;
5949 _postToServer(state2, id);
5950 }, SYNC_DEBOUNCE_MS);
5951 }
5952 let _pendingActivityWindowId = null;
5953 function _postToServer(state2, windowId) {
5954 const config = window.desktopModeConfig;
5955 const url = config?.osSettingsUrl;
5956 const nonce = config?.restNonce;
5957 if (!url || !nonce) {
5958 _emitSaveLifecycle("saved");
5959 return;
5960 }
5961 _emitSaveLifecycle("saving");
5962 const attributedWindowId = windowId || "desktop-mode-os-settings";
5963 trackedFetch$1(
5964 url,
5965 {
5966 method: "POST",
5967 headers: {
5968 "Content-Type": "application/json",
5969 "X-WP-Nonce": nonce
5970 },
5971 body: JSON.stringify({ settings: state2 })
5972 },
5973 { windowId: attributedWindowId }
5974 ).then((res) => {
5975 if (!res.ok) {
5976 throw new Error(`${res.status} ${res.statusText}`);
5977 }
5978 _lastConfirmedState = _cloneState(state2);
5979 _emitSaveLifecycle("saved");
5980 }).catch((err) => {
5981 if (_lastConfirmedState) {
5982 _writeLocalStorage(_lastConfirmedState);
5983 _emitSaveLifecycle(
5984 "failed",
5985 err instanceof Error ? err.message : String(err),
5986 _cloneState(_lastConfirmedState)
5987 );
5988 } else {
5989 _emitSaveLifecycle(
5990 "failed",
5991 err instanceof Error ? err.message : String(err)
5992 );
5993 }
5994 });
5995 }
5996 function _emitSaveLifecycle(phase, error, rolledBackTo) {
5997 const detail = { phase };
5998 if (error) {
5999 detail.error = error;
6000 }
6001 if (rolledBackTo) {
6002 detail.rolledBackTo = rolledBackTo;
6003 }
6004 document.dispatchEvent(
6005 new CustomEvent("desktop-mode-os-settings-save-lifecycle", { detail })
6006 );
6007 }
6008 function structuredDefaults() {
6009 return {
6010 ...DEFAULTS,
6011 customGradient: { ...DEFAULTS.customGradient },
6012 customImage: null,
6013 wallpaperSettings: { ...DEFAULTS.wallpaperSettings },
6014 ai: { ...DEFAULTS.ai },
6015 // Clone the collection fields too. A shallow `...DEFAULTS`
6016 // aliases these nested objects, so a later in-place mutation
6017 // (e.g. dragging the gradient editor after a Reset, which spreads
6018 // these defaults into live state) would corrupt the module-level
6019 // DEFAULTS singleton for the rest of the session.
6020 //
6021 // These are one-level clones, which is sufficient *because* all
6022 // three defaults are empty (`{}` / `[]`) — there are no inner
6023 // objects to share. If `DEFAULTS.dockPromotedPositions` ever
6024 // ships seeded entries, its `{ x, y }` values would need a
6025 // deeper clone here.
6026 itemVisibility: { ...DEFAULTS.itemVisibility },
6027 dockOrder: [...DEFAULTS.dockOrder],
6028 dockPromotedPositions: { ...DEFAULTS.dockPromotedPositions }
6029 };
6030 }
6031 function sanitizeAi(raw) {
6032 if (!raw || typeof raw !== "object") {
6033 return { ...DEFAULTS.ai };
6034 }
6035 const { enabled } = raw;
6036 return {
6037 enabled: typeof enabled === "boolean" ? enabled : DEFAULTS.ai.enabled
6038 };
6039 }
6040 function sanitizeCustomGradient(raw) {
6041 if (!raw || typeof raw !== "object") {
6042 return { ...DEFAULTS.customGradient };
6043 }
6044 const { from, to, angle } = raw;
6045 return {
6046 from: isHexColor(from) ? from : DEFAULTS.customGradient.from,
6047 to: isHexColor(to) ? to : DEFAULTS.customGradient.to,
6048 angle: typeof angle === "number" && Number.isFinite(angle) && angle >= 0 && angle <= 360 ? angle : DEFAULTS.customGradient.angle
6049 };
6050 }
6051 function sanitizeCustomImage(raw) {
6052 if (!raw || typeof raw !== "object") {
6053 return null;
6054 }
6055 const { id, url } = raw;
6056 if (typeof id !== "number" || !Number.isFinite(id) || id <= 0) {
6057 return null;
6058 }
6059 if (typeof url !== "string" || !/^https?:\/\//i.test(url)) {
6060 return null;
6061 }
6062 return { id, url };
6063 }
6064 const store$f = createSharedStore(
6065 "desktop-mode/dock-rail-registry",
6066 () => ({
6067 registry: /* @__PURE__ */ new Map(),
6068 listeners: /* @__PURE__ */ new Set(),
6069 activeId: "default"
6070 })
6071 );
6072 const registry$a = store$f.state.registry;
6073 const listeners$d = store$f.state.listeners;
6074 const ID_RE = /^[a-z0-9_-]+$/;
6075 function register$2(renderer) {
6076 if (!renderer || typeof renderer !== "object") {
6077 throw new TypeError(
6078 "[desktop-mode] registerDockRailRenderer: renderer must be an object."
6079 );
6080 }
6081 if (typeof renderer.id !== "string" || !ID_RE.test(renderer.id)) {
6082 throw new TypeError(
6083 `[desktop-mode] registerDockRailRenderer: id must match /^[a-z0-9_-]+$/, got: ${String(renderer.id)}`
6084 );
6085 }
6086 if (typeof renderer.label !== "string" || renderer.label === "") {
6087 throw new TypeError(
6088 "[desktop-mode] registerDockRailRenderer: label must be a non-empty string."
6089 );
6090 }
6091 if (typeof renderer.mount !== "function") {
6092 throw new TypeError(
6093 "[desktop-mode] registerDockRailRenderer: mount must be a function."
6094 );
6095 }
6096 if (renderer.apiVersion !== void 0 && renderer.apiVersion !== 1) {
6097 throw new TypeError(
6098 `[desktop-mode] registerDockRailRenderer: unsupported apiVersion ${renderer.apiVersion} (this shell speaks v1).`
6099 );
6100 }
6101 registry$a.set(renderer.id, renderer);
6102 notify$g();
6103 }
6104 function unregister$2(id) {
6105 if (registry$a.delete(id)) {
6106 notify$g();
6107 }
6108 }
6109 function unregisterByOwner$1(owner) {
6110 if (!owner) {
6111 return 0;
6112 }
6113 let removed = 0;
6114 for (const [id, renderer] of Array.from(registry$a.entries())) {
6115 if (renderer.owner === owner) {
6116 registry$a.delete(id);
6117 removed++;
6118 }
6119 }
6120 if (removed > 0) {
6121 notify$g();
6122 }
6123 return removed;
6124 }
6125 function list() {
6126 return Array.from(registry$a.values());
6127 }
6128 function subscribe$4(cb) {
6129 listeners$d.add(cb);
6130 return () => {
6131 listeners$d.delete(cb);
6132 };
6133 }
6134 function setActiveRenderer(id) {
6135 if (store$f.state.activeId === id) {
6136 return;
6137 }
6138 store$f.state.activeId = id;
6139 notify$g();
6140 }
6141 function resolveActive() {
6142 return registry$a.get(store$f.state.activeId) ?? registry$a.get("default") ?? registry$a.values().next().value;
6143 }
6144 function notify$g() {
6145 const snapshot = Array.from(listeners$d);
6146 for (const cb of snapshot) {
6147 try {
6148 cb();
6149 } catch (err) {
6150 if (typeof console !== "undefined") {
6151 console.error(
6152 "[desktop-mode] dock-rail-renderer listener threw:",
6153 err
6154 );
6155 }
6156 }
6157 }
6158 }
6159 const SHOW_DELAY_MS = 180;
6160 const HIDE_DELAY_MS = 220;
6161 const STAGGER_MS = 32;
6162 function attachDockPeek(deps2) {
6163 const { tile: tile2 } = deps2;
6164 let popover = null;
6165 let showTimer = null;
6166 let hideTimer = null;
6167 let inside = false;
6168 const cancelShow = () => {
6169 if (showTimer !== null) {
6170 window.clearTimeout(showTimer);
6171 showTimer = null;
6172 }
6173 };
6174 const cancelHide = () => {
6175 if (hideTimer !== null) {
6176 window.clearTimeout(hideTimer);
6177 hideTimer = null;
6178 }
6179 };
6180 const tearDown = () => {
6181 cancelShow();
6182 cancelHide();
6183 tile2.removeAttribute("data-peek-active");
6184 if (popover) {
6185 popover.remove();
6186 popover = null;
6187 }
6188 deps2.suppressTooltip(false);
6189 };
6190 const onPointerEnterTile = (e) => {
6191 if (e.pointerType !== "mouse") {
6192 return;
6193 }
6194 if (!shouldShowPeek(deps2)) {
6195 return;
6196 }
6197 inside = true;
6198 cancelHide();
6199 if (popover) {
6200 return;
6201 }
6202 showTimer = window.setTimeout(() => {
6203 showTimer = null;
6204 if (!inside) {
6205 return;
6206 }
6207 showPeek();
6208 }, SHOW_DELAY_MS);
6209 };
6210 const onPointerLeaveTile = (e) => {
6211 if (popover && e.relatedTarget instanceof Node && popover.contains(e.relatedTarget)) {
6212 return;
6213 }
6214 inside = false;
6215 cancelShow();
6216 scheduleHide();
6217 };
6218 const scheduleHide = () => {
6219 cancelHide();
6220 hideTimer = window.setTimeout(() => {
6221 hideTimer = null;
6222 if (inside) {
6223 return;
6224 }
6225 tearDown();
6226 }, HIDE_DELAY_MS);
6227 };
6228 const showPeek = () => {
6229 deps2.suppressTooltip(true);
6230 tile2.setAttribute("data-peek-active", "");
6231 popover = buildPopover(deps2, () => tearDown());
6232 document.body.appendChild(popover);
6233 inheritShellSchemeVars(popover);
6234 positionPopover(popover, tile2, deps2.getOrientation());
6235 requestAnimationFrame(() => {
6236 popover?.classList.add("desktop-mode-dock-peek--open");
6237 });
6238 popover.addEventListener("pointerenter", () => {
6239 inside = true;
6240 cancelHide();
6241 });
6242 popover.addEventListener("pointerleave", (e) => {
6243 if (e.relatedTarget instanceof Node && tile2.contains(e.relatedTarget)) {
6244 return;
6245 }
6246 inside = false;
6247 scheduleHide();
6248 });
6249 };
6250 tile2.addEventListener("pointerenter", onPointerEnterTile);
6251 tile2.addEventListener("pointerleave", onPointerLeaveTile);
6252 return () => {
6253 tile2.removeEventListener("pointerenter", onPointerEnterTile);
6254 tile2.removeEventListener("pointerleave", onPointerLeaveTile);
6255 tearDown();
6256 };
6257 }
6258 function shouldShowPeek(deps2) {
6259 return deps2.getInstances().length >= 1;
6260 }
6261 function buildPopover(deps2, dismiss) {
6262 const root = document.createElement("div");
6263 root.className = "desktop-mode-dock-peek";
6264 root.setAttribute("role", "menu");
6265 root.setAttribute("aria-label", sprintf(
6266 // translators: %s is the dock item's admin-page title (e.g., "Posts")
6267 __("%s — open windows"),
6268 deps2.item.title
6269 ));
6270 const cards = document.createElement("div");
6271 cards.className = "desktop-mode-dock-peek__cards";
6272 root.appendChild(cards);
6273 const instances = deps2.getInstances();
6274 let cardIndex = 0;
6275 for (const win of instances) {
6276 const card = buildInstanceCard(win, deps2, cardIndex++, dismiss);
6277 cards.appendChild(card);
6278 }
6279 if (deps2.enableGhost !== false) {
6280 const ghost = buildGhostCard(deps2, cardIndex, dismiss);
6281 cards.appendChild(ghost);
6282 }
6283 return root;
6284 }
6285 function restoreIfMinimized(win, card) {
6286 if (win.state !== "minimized") {
6287 return;
6288 }
6289 win.restore();
6290 if (card) {
6291 delete card.dataset.state;
6292 }
6293 }
6294 function buildInstanceCard(win, deps2, index2, dismiss) {
6295 const card = document.createElement("button");
6296 card.type = "button";
6297 card.setAttribute("role", "menuitem");
6298 card.className = "desktop-mode-dock-peek__card desktop-mode-dock-peek__card--instance";
6299 card.style.setProperty("--peek-card-index", String(index2));
6300 card.style.setProperty(
6301 "--peek-card-delay",
6302 `${index2 * STAGGER_MS}ms`
6303 );
6304 const title = win.config.title || deps2.item.title;
6305 card.style.setProperty(
6306 "--peek-card-hue",
6307 `${hashTitleToHue(win.id || title)}`
6308 );
6309 card.style.setProperty(
6310 "--peek-card-vt-name",
6311 `desktop-mode-peek-card-${win.id}`
6312 );
6313 const titlebar = document.createElement("span");
6314 titlebar.className = "desktop-mode-dock-peek__card-titlebar";
6315 const dots = document.createElement("span");
6316 dots.className = "desktop-mode-dock-peek__card-dots";
6317 dots.setAttribute("aria-hidden", "true");
6318 for (let i = 0; i < 3; i++) {
6319 dots.appendChild(document.createElement("i"));
6320 }
6321 titlebar.appendChild(dots);
6322 const iconHost = document.createElement("span");
6323 iconHost.className = "desktop-mode-dock-peek__card-icon";
6324 iconHost.setAttribute("aria-hidden", "true");
6325 const iconCls = win.config.icon || deps2.item.icon;
6326 if (iconCls.startsWith("dashicons-")) {
6327 iconHost.classList.add("dashicons", sanitizeClassName(iconCls));
6328 } else {
6329 iconHost.classList.add("dashicons", "dashicons-admin-generic");
6330 }
6331 titlebar.appendChild(iconHost);
6332 const label = document.createElement("span");
6333 label.className = "desktop-mode-dock-peek__card-label";
6334 label.textContent = title;
6335 titlebar.appendChild(label);
6336 card.appendChild(titlebar);
6337 const defaultBody = document.createElement("span");
6338 defaultBody.className = "desktop-mode-dock-peek__card-body";
6339 defaultBody.setAttribute("aria-hidden", "true");
6340 for (let i = 0; i < 3; i++) {
6341 const line = document.createElement("span");
6342 line.className = "desktop-mode-dock-peek__card-line";
6343 defaultBody.appendChild(line);
6344 }
6345 const ctx = { window: win, item: deps2.item };
6346 const body = applyFilters(
6347 HOOKS.DOCK_PEEK_CARD_CONTENT,
6348 defaultBody,
6349 ctx
6350 );
6351 if (body !== defaultBody) {
6352 body.classList.add("desktop-mode-dock-peek__card-body--custom");
6353 }
6354 card.appendChild(body);
6355 if (win.state === "minimized") {
6356 card.dataset.state = "minimized";
6357 }
6358 card.addEventListener("click", () => {
6359 spawnFocusViewTransition(deps2, win, card, dismiss);
6360 });
6361 card.addEventListener("pointerenter", () => {
6362 if (win.state === "minimized") {
6363 restoreIfMinimized(win, card);
6364 } else if (deps2.windowManager.getFocused() !== win) {
6365 deps2.windowManager.focus(win);
6366 }
6367 });
6368 const finalCard = applyFilters(
6369 HOOKS.DOCK_PEEK_CARD_ELEMENT,
6370 card,
6371 ctx
6372 );
6373 return finalCard;
6374 }
6375 function spawnFocusViewTransition(deps2, win, card, dismiss) {
6376 const doc = document;
6377 const vtName = `desktop-mode-peek-card-${win.id}`;
6378 const focus = () => {
6379 dismiss();
6380 restoreIfMinimized(win, card);
6381 deps2.windowManager.focus(win);
6382 };
6383 if (typeof doc.startViewTransition !== "function") {
6384 focus();
6385 return;
6386 }
6387 const targetEl = win.element;
6388 card.style.setProperty("view-transition-name", vtName);
6389 targetEl.style.setProperty("view-transition-name", vtName);
6390 const transition = doc.startViewTransition(focus);
6391 const cleanup = () => {
6392 card.style.removeProperty("view-transition-name");
6393 targetEl.style.removeProperty("view-transition-name");
6394 };
6395 const t = transition;
6396 if (t.finished && typeof t.finished.then === "function") {
6397 t.finished.then(cleanup, cleanup);
6398 } else {
6399 Promise.resolve().then(cleanup);
6400 }
6401 }
6402 function buildGhostCard(deps2, index2, dismiss) {
6403 const card = document.createElement("button");
6404 card.type = "button";
6405 card.setAttribute("role", "menuitem");
6406 card.className = "desktop-mode-dock-peek__card desktop-mode-dock-peek__card--ghost";
6407 card.style.setProperty("--peek-card-index", String(index2));
6408 card.style.setProperty(
6409 "--peek-card-delay",
6410 `${index2 * STAGGER_MS}ms`
6411 );
6412 const plus = document.createElement("span");
6413 plus.className = "desktop-mode-dock-peek__card-plus";
6414 plus.setAttribute("aria-hidden", "true");
6415 plus.textContent = "+";
6416 card.appendChild(plus);
6417 const label = document.createElement("span");
6418 label.className = "desktop-mode-dock-peek__card-label";
6419 label.textContent = sprintf(
6420 // translators: %s is the admin-page title (e.g., "Posts")
6421 __("New %s"),
6422 deps2.item.title
6423 );
6424 card.appendChild(label);
6425 card.addEventListener("click", () => {
6426 spawnWithViewTransition(deps2, dismiss);
6427 });
6428 return card;
6429 }
6430 function spawnWithViewTransition(deps2, dismiss) {
6431 const doc = document;
6432 const spawn = () => {
6433 dismiss();
6434 deps2.openNew();
6435 };
6436 if (typeof doc.startViewTransition === "function") {
6437 doc.startViewTransition(spawn);
6438 return;
6439 }
6440 spawn();
6441 }
6442 const VIEWPORT_MARGIN_PX = 12;
6443 const SHELL_SCHEME_VARS = [
6444 "--wp-admin-theme-color",
6445 "--desktop-mode-titlebar-bg",
6446 "--desktop-mode-titlebar-bg-focused",
6447 "--desktop-mode-titlebar-color",
6448 "--desktop-mode-titlebar-color-focused"
6449 ];
6450 function inheritShellSchemeVars(popover) {
6451 const shell = document.querySelector(".desktop-mode-shell");
6452 if (!shell) {
6453 return;
6454 }
6455 const computed = window.getComputedStyle(shell);
6456 for (const name of SHELL_SCHEME_VARS) {
6457 const value = computed.getPropertyValue(name).trim();
6458 if (value) {
6459 popover.style.setProperty(name, value);
6460 }
6461 }
6462 }
6463 function positionPopover(popover, tile2, orientation) {
6464 const rect = tile2.getBoundingClientRect();
6465 popover.dataset.orientation = orientation;
6466 if (orientation === "bottom") {
6467 popover.style.left = `${rect.left + rect.width / 2}px`;
6468 popover.style.top = `${rect.top - 12}px`;
6469 } else if (orientation === "right") {
6470 popover.style.top = `${rect.top + rect.height / 2}px`;
6471 popover.style.left = `${rect.left - 12}px`;
6472 } else {
6473 popover.style.top = `${rect.top + rect.height / 2}px`;
6474 popover.style.left = `${rect.right + 12}px`;
6475 }
6476 requestAnimationFrame(() => clampToViewport$1(popover));
6477 }
6478 function clampToViewport$1(popover, orientation) {
6479 const rect = popover.getBoundingClientRect();
6480 const vh = window.innerHeight;
6481 const vw = window.innerWidth;
6482 const min = VIEWPORT_MARGIN_PX;
6483 let dy = 0;
6484 let dx = 0;
6485 if (rect.top < min) {
6486 dy = min - rect.top;
6487 } else if (rect.bottom > vh - min) {
6488 dy = vh - min - rect.bottom;
6489 }
6490 if (rect.left < min) {
6491 dx = min - rect.left;
6492 } else if (rect.right > vw - min) {
6493 dx = vw - min - rect.right;
6494 }
6495 if (dx === 0 && dy === 0) {
6496 return;
6497 }
6498 popover.style.setProperty("--peek-clamp-x", `${dx}px`);
6499 popover.style.setProperty("--peek-clamp-y", `${dy}px`);
6500 popover.classList.add("desktop-mode-dock-peek--clamped");
6501 }
6502 function tryOpenExternalUrl(url) {
6503 try {
6504 const parsed = new URL(url, window.location.origin);
6505 if (parsed.origin === window.location.origin) {
6506 return false;
6507 }
6508 window.open(parsed.toString(), "_blank", "noopener,noreferrer");
6509 return true;
6510 } catch {
6511 return false;
6512 }
6513 }
6514 function synthDockId(desktopIconId) {
6515 return `desktop:${desktopIconId}`;
6516 }
6517 function synthIconId(dockItemId) {
6518 return `dock:${dockItemId}`;
6519 }
6520 function canonicalItemId(id) {
6521 if (id.startsWith("dock:")) {
6522 return id.slice(5);
6523 }
6524 if (id.startsWith("desktop:")) {
6525 return id.slice(8);
6526 }
6527 return id;
6528 }
6529 function resolvePlacement(id, nativeRail, visibility) {
6530 const override = visibility[id];
6531 if (override) {
6532 return override;
6533 }
6534 return nativeRail;
6535 }
6536 function shouldShowOnDock(placement) {
6537 return placement === "dock" || placement === "both";
6538 }
6539 function shouldShowOnDesktop(placement) {
6540 return placement === "desktop" || placement === "both";
6541 }
6542 function applyDockPlacement(dockItems, desktopIcons, settings, dockedNativeWindows) {
6543 const visibility = settings.itemVisibility;
6544 const order = settings.dockOrder;
6545 const kept = [];
6546 for (const item of dockItems) {
6547 const placement = resolvePlacement(item.id, "dock", visibility);
6548 if (shouldShowOnDock(placement)) {
6549 kept.push(item);
6550 }
6551 }
6552 for (const icon of desktopIcons) {
6553 const placement = resolvePlacement(icon.id, "desktop", visibility);
6554 if (!shouldShowOnDock(placement)) {
6555 continue;
6556 }
6557 if (icon.window && dockedNativeWindows && dockedNativeWindows.has(icon.window)) {
6558 continue;
6559 }
6560 kept.push({
6561 id: synthIconId(icon.id),
6562 title: icon.title,
6563 icon: icon.icon,
6564 url: icon.url || "",
6565 // Carry the native-window id forward so the dock can light
6566 // the active-dot indicator + show the hover-peek card when
6567 // the target window is open. Without this, window-target
6568 // icons (no `url`) synthesize a tile whose only id-bearing
6569 // field is an empty string — deriveWindowId('') matches
6570 // nothing the window manager has stored.
6571 windowId: icon.window || void 0,
6572 badge: 0,
6573 submenu: [],
6574 isCore: false
6575 });
6576 }
6577 return applyOrder(kept, order);
6578 }
6579 function applyDesktopPlacement(desktopIcons, dockItems, visibility) {
6580 const out = [];
6581 for (const icon of desktopIcons) {
6582 const placement = resolvePlacement(icon.id, "desktop", visibility);
6583 if (shouldShowOnDesktop(placement)) {
6584 out.push(icon);
6585 }
6586 }
6587 let synthIndex = 0;
6588 for (const item of dockItems) {
6589 const placement = resolvePlacement(item.id, "dock", visibility);
6590 if (!shouldShowOnDesktop(placement)) {
6591 continue;
6592 }
6593 out.push({
6594 id: synthDockId(item.id),
6595 title: item.title,
6596 icon: item.icon,
6597 window: "",
6598 url: item.url || "",
6599 // Place synthesized dock-promoted icons after server-registered
6600 // ones. Stable ordering by source-list index inside the bucket.
6601 position: 2e3 + synthIndex++
6602 });
6603 }
6604 return out;
6605 }
6606 function applyOrder(items, order) {
6607 if (order.length === 0 || items.length <= 1) {
6608 return items;
6609 }
6610 const byId = /* @__PURE__ */ new Map();
6611 for (const item of items) {
6612 byId.set(item.id, item);
6613 }
6614 const out = [];
6615 const placed = /* @__PURE__ */ new Set();
6616 for (const id of order) {
6617 const item = byId.get(id);
6618 if (item) {
6619 out.push(item);
6620 placed.add(id);
6621 }
6622 }
6623 for (const item of items) {
6624 if (!placed.has(item.id)) {
6625 out.push(item);
6626 }
6627 }
6628 return out;
6629 }
6630 function html(strings, ...values) {
6631 return { __wpdHtml: true, strings, values };
6632 }
6633 function isTemplateResult(v) {
6634 return !!v && v.__wpdHtml === true;
6635 }
6636 const MARKER_PREFIX = "$$wpd$$";
6637 const MARKER_RE = /\$\$wpd\$\$(\d+)\$\$/g;
6638 function joinWithMarkers(strings) {
6639 let out = strings[0];
6640 for (let i = 1; i < strings.length; i++) {
6641 out += `${MARKER_PREFIX}${i - 1}$$` + strings[i];
6642 }
6643 return out;
6644 }
6645 const compiledCache = /* @__PURE__ */ new WeakMap();
6646 function compile(strings) {
6647 const cached = compiledCache.get(strings);
6648 if (cached) {
6649 return cached;
6650 }
6651 const template = document.createElement("template");
6652 template.innerHTML = joinWithMarkers(strings);
6653 const recipes = [];
6654 const walk2 = (node, path) => {
6655 if (node.nodeType === Node.ELEMENT_NODE) {
6656 const el = node;
6657 for (const attr of Array.from(el.attributes)) {
6658 const rawName = attr.name;
6659 const rawValue = attr.value;
6660 const prefix = rawName[0];
6661 if (MARKER_RE.test(rawValue)) {
6662 MARKER_RE.lastIndex = 0;
6663 if (prefix === "@") {
6664 const match = MARKER_RE.exec(rawValue);
6665 MARKER_RE.lastIndex = 0;
6666 recipes.push({
6667 path,
6668 kind: "event",
6669 name: rawName.slice(1),
6670 valueIndex: match ? Number(match[1]) : 0
6671 });
6672 el.removeAttribute(rawName);
6673 } else if (prefix === ".") {
6674 const match = MARKER_RE.exec(rawValue);
6675 MARKER_RE.lastIndex = 0;
6676 recipes.push({
6677 path,
6678 kind: "prop",
6679 name: rawName.slice(1),
6680 valueIndex: match ? Number(match[1]) : 0
6681 });
6682 el.removeAttribute(rawName);
6683 } else if (prefix === "?") {
6684 const match = MARKER_RE.exec(rawValue);
6685 MARKER_RE.lastIndex = 0;
6686 recipes.push({
6687 path,
6688 kind: "bool",
6689 name: rawName.slice(1),
6690 valueIndex: match ? Number(match[1]) : 0
6691 });
6692 el.removeAttribute(rawName);
6693 } else {
6694 const fragments = [];
6695 const indices = [];
6696 let lastEnd = 0;
6697 let m;
6698 MARKER_RE.lastIndex = 0;
6699 while ((m = MARKER_RE.exec(rawValue)) !== null) {
6700 fragments.push(rawValue.slice(lastEnd, m.index));
6701 indices.push(Number(m[1]));
6702 lastEnd = m.index + m[0].length;
6703 }
6704 fragments.push(rawValue.slice(lastEnd));
6705 recipes.push({
6706 path,
6707 kind: "attr",
6708 name: rawName,
6709 template: fragments,
6710 valueIndices: indices
6711 });
6712 el.setAttribute(rawName, "");
6713 }
6714 }
6715 }
6716 }
6717 const children = Array.from(node.childNodes);
6718 let shift = 0;
6719 for (let i = 0; i < children.length; i++) {
6720 const child = children[i];
6721 const liveIndex = i + shift;
6722 if (child.nodeType === Node.TEXT_NODE) {
6723 const text = child.textContent || "";
6724 if (!MARKER_RE.test(text)) {
6725 MARKER_RE.lastIndex = 0;
6726 continue;
6727 }
6728 MARKER_RE.lastIndex = 0;
6729 const parent = child.parentNode;
6730 let lastEnd = 0;
6731 let m;
6732 const newNodes = [];
6733 const newRecipes = [];
6734 MARKER_RE.lastIndex = 0;
6735 while ((m = MARKER_RE.exec(text)) !== null) {
6736 if (m.index > lastEnd) {
6737 newNodes.push(document.createTextNode(text.slice(lastEnd, m.index)));
6738 }
6739 const placeholder = document.createTextNode("");
6740 newNodes.push(placeholder);
6741 newRecipes.push({
6742 path: [...path, liveIndex + newNodes.length - 1],
6743 kind: "node",
6744 valueIndex: Number(m[1])
6745 });
6746 lastEnd = m.index + m[0].length;
6747 }
6748 if (lastEnd < text.length) {
6749 newNodes.push(document.createTextNode(text.slice(lastEnd)));
6750 }
6751 for (const nn of newNodes) {
6752 parent.insertBefore(nn, child);
6753 }
6754 parent.removeChild(child);
6755 shift += newNodes.length - 1;
6756 recipes.push(...newRecipes);
6757 } else {
6758 walk2(child, [...path, liveIndex]);
6759 }
6760 }
6761 };
6762 walk2(template.content, []);
6763 const buildParts = (fragment) => {
6764 const out = [];
6765 for (const r of recipes) {
6766 let node = fragment;
6767 for (const idx of r.path) {
6768 node = node.childNodes[idx];
6769 }
6770 if (r.kind === "node") {
6771 out.push({
6772 kind: "node",
6773 valueIndex: r.valueIndex,
6774 child: {
6775 anchor: node,
6776 state: null
6777 }
6778 });
6779 } else if (r.kind === "attr") {
6780 out.push({
6781 kind: "attr",
6782 element: node,
6783 name: r.name,
6784 template: r.template,
6785 valueIndices: r.valueIndices
6786 });
6787 } else if (r.kind === "event") {
6788 out.push({
6789 kind: "event",
6790 valueIndex: r.valueIndex,
6791 element: node,
6792 name: r.name
6793 });
6794 } else if (r.kind === "prop") {
6795 out.push({
6796 kind: "prop",
6797 valueIndex: r.valueIndex,
6798 element: node,
6799 name: r.name
6800 });
6801 } else if (r.kind === "bool") {
6802 out.push({
6803 kind: "bool",
6804 valueIndex: r.valueIndex,
6805 element: node,
6806 name: r.name
6807 });
6808 }
6809 }
6810 return out;
6811 };
6812 const entry = { template, buildParts };
6813 compiledCache.set(strings, entry);
6814 return entry;
6815 }
6816 const mountState = /* @__PURE__ */ new WeakMap();
6817 function mountIntact(state2, container) {
6818 for (const node of state2.nodes) {
6819 if (node.parentNode !== container) {
6820 return false;
6821 }
6822 }
6823 return true;
6824 }
6825 function render$1(result, container) {
6826 const existing = mountState.get(container);
6827 if (existing && existing.strings === result.strings && mountIntact(existing, container)) {
6828 applyValues(existing.parts, result.values);
6829 return;
6830 }
6831 const compiled = compile(result.strings);
6832 const fragment = compiled.template.content.cloneNode(true);
6833 const parts = compiled.buildParts(fragment);
6834 const nodes = Array.from(fragment.childNodes);
6835 while (container.firstChild) {
6836 container.removeChild(container.firstChild);
6837 }
6838 container.appendChild(fragment);
6839 applyValues(parts, result.values);
6840 mountState.set(container, { strings: result.strings, parts, nodes });
6841 }
6842 function applyValues(parts, values) {
6843 for (const part of parts) {
6844 if (part.kind === "node") {
6845 updateChildPart(part.child, values[part.valueIndex]);
6846 } else if (part.kind === "attr") {
6847 let composed = part.template[0];
6848 for (let i = 0; i < part.valueIndices.length; i++) {
6849 composed += formatText(values[part.valueIndices[i]]);
6850 composed += part.template[i + 1];
6851 }
6852 if (composed !== part.last) {
6853 part.last = composed;
6854 if (composed === "") {
6855 part.element.removeAttribute(part.name);
6856 } else {
6857 part.element.setAttribute(part.name, composed);
6858 }
6859 }
6860 } else if (part.kind === "event") {
6861 const next = values[part.valueIndex];
6862 if (next !== part.current) {
6863 if (part.current) {
6864 part.element.removeEventListener(part.name, part.current);
6865 }
6866 if (next) {
6867 part.element.addEventListener(part.name, next);
6868 }
6869 part.current = next;
6870 }
6871 } else if (part.kind === "prop") {
6872 const next = values[part.valueIndex];
6873 if (next !== part.last) {
6874 part.last = next;
6875 part.element[part.name] = next;
6876 }
6877 } else if (part.kind === "bool") {
6878 const next = !!values[part.valueIndex];
6879 if (next !== part.last) {
6880 part.last = next;
6881 if (next) {
6882 part.element.setAttribute(part.name, "");
6883 } else {
6884 part.element.removeAttribute(part.name);
6885 }
6886 }
6887 }
6888 }
6889 }
6890 function updateChildPart(child, value) {
6891 if (value === null || value === void 0 || value === false) {
6892 if (child.state) {
6893 disposeChildState(child.state);
6894 child.state = null;
6895 }
6896 return;
6897 }
6898 if (Array.isArray(value)) {
6899 updateArrayChild(child, value);
6900 return;
6901 }
6902 if (isTemplateResult(value)) {
6903 updateTemplateChild(child, value);
6904 return;
6905 }
6906 if (value instanceof Node) {
6907 updateNodeChild(child, value);
6908 return;
6909 }
6910 updateTextChild(child, formatText(value));
6911 }
6912 function updateNodeChild(child, node) {
6913 const old = child.state;
6914 if (old?.shape === "node" && old.node === node) {
6915 return;
6916 }
6917 if (old) {
6918 disposeChildState(old);
6919 }
6920 insertBeforeAnchor(child, [node]);
6921 child.state = { shape: "node", node };
6922 }
6923 function updateTextChild(child, text) {
6924 const old = child.state;
6925 if (old?.shape === "text") {
6926 if (old.text !== text) {
6927 old.node.textContent = text;
6928 old.text = text;
6929 }
6930 return;
6931 }
6932 if (old) {
6933 disposeChildState(old);
6934 }
6935 const node = document.createTextNode(text);
6936 insertBeforeAnchor(child, [node]);
6937 child.state = { shape: "text", node, text };
6938 }
6939 function updateTemplateChild(child, result) {
6940 const old = child.state;
6941 if (old?.shape === "template" && old.strings === result.strings) {
6942 applyValues(old.parts, result.values);
6943 return;
6944 }
6945 if (old) {
6946 disposeChildState(old);
6947 }
6948 const compiled = compile(result.strings);
6949 const fragment = compiled.template.content.cloneNode(true);
6950 const parts = compiled.buildParts(fragment);
6951 const topNodes = Array.from(fragment.childNodes);
6952 insertBeforeAnchor(child, [fragment]);
6953 applyValues(parts, result.values);
6954 child.state = {
6955 shape: "template",
6956 strings: result.strings,
6957 parts,
6958 nodes: topNodes
6959 };
6960 }
6961 function updateArrayChild(child, arr) {
6962 const old = child.state;
6963 if (old?.shape === "array" && old.entries.length === arr.length) {
6964 for (let i = 0; i < arr.length; i++) {
6965 updateChildPart(old.entries[i], arr[i]);
6966 }
6967 return;
6968 }
6969 if (old) {
6970 disposeChildState(old);
6971 }
6972 const entries = [];
6973 for (const v of arr) {
6974 const entryAnchor = document.createTextNode("");
6975 insertBeforeAnchor(child, [entryAnchor]);
6976 const entry = { anchor: entryAnchor, state: null };
6977 updateChildPart(entry, v);
6978 entries.push(entry);
6979 }
6980 child.state = { shape: "array", entries };
6981 }
6982 function insertBeforeAnchor(child, nodes) {
6983 const parent = child.anchor.parentNode;
6984 if (!parent) {
6985 return;
6986 }
6987 for (const node of nodes) {
6988 parent.insertBefore(node, child.anchor);
6989 }
6990 }
6991 function disposeChildState(state2) {
6992 if (state2.shape === "text") {
6993 state2.node.remove();
6994 return;
6995 }
6996 if (state2.shape === "template") {
6997 for (const node of state2.nodes) {
6998 if (node.parentNode) {
6999 node.parentNode.removeChild(node);
7000 }
7001 }
7002 return;
7003 }
7004 if (state2.shape === "node") {
7005 if (state2.node.parentNode) {
7006 state2.node.parentNode.removeChild(state2.node);
7007 }
7008 return;
7009 }
7010 for (const entry of state2.entries) {
7011 if (entry.state) {
7012 disposeChildState(entry.state);
7013 }
7014 entry.anchor.remove();
7015 }
7016 }
7017 function formatText(v) {
7018 if (v === null || v === void 0 || v === false) {
7019 return "";
7020 }
7021 return String(v);
7022 }
7023 const _Component = class _Component extends HTMLElement {
7024 constructor() {
7025 super();
7026 this._renderScheduled = false;
7027 this._propValues = {};
7028 const ctor = this.constructor;
7029 if (ctor.shadow) {
7030 this.attachShadow({ mode: "open" });
7031 this._renderRoot = this.shadowRoot;
7032 } else {
7033 this._renderRoot = this;
7034 }
7035 this._installPropAccessors();
7036 }
7037 static get observedAttributes() {
7038 return this.props.map(kebab);
7039 }
7040 connectedCallback() {
7041 this._adoptStyles();
7042 this.requestUpdate();
7043 }
7044 attributeChangedCallback(name, oldValue, newValue) {
7045 if (oldValue === newValue) {
7046 return;
7047 }
7048 const prop2 = camel(name);
7049 this._propValues[prop2] = newValue;
7050 this.requestUpdate();
7051 }
7052 /**
7053 * Declarative class-name setter. Assign an array (or a
7054 * space-separated string) and the host's `class` attribute is
7055 * rewritten to match. Intended for programmatic styling — when
7056 * a plugin has enqueued its own stylesheet and wants to apply
7057 * one of those classes to a shell component:
7058 *
7059 * ```js
7060 * element.classNames = [ 'my-plugin-brand', 'is-active' ];
7061 * // → <wpd-select class="my-plugin-brand is-active">
7062 * ```
7063 *
7064 * The plain HTML `class="…"` attribute works just the same and
7065 * is always preferred when writing markup by hand — this setter
7066 * exists for the JS-API case where the caller has an array of
7067 * conditional classes in hand.
7068 *
7069 * Getter returns the current `classList` as a plain array for
7070 * symmetric read/write.
7071 *
7072 * @since 0.5.0
7073 */
7074 get classNames() {
7075 return Array.from(this.classList);
7076 }
7077 set classNames(next) {
7078 if (next === null || next === void 0) {
7079 this.removeAttribute("class");
7080 return;
7081 }
7082 const list2 = Array.isArray(next) ? next : String(next).split(/\s+/);
7083 const cleaned = list2.map((s) => String(s).trim()).filter((s) => s !== "");
7084 this.className = cleaned.join(" ");
7085 }
7086 /**
7087 * Request a re-render explicitly. Components rarely need this —
7088 * declare state via props + attribute observers and the render
7089 * loop picks up changes automatically.
7090 */
7091 requestUpdate() {
7092 this._scheduleRender();
7093 }
7094 /**
7095 * Dispatch a `CustomEvent` with a `detail`. Bubbles + composed
7096 * by default (matches typical WC UX — events cross shadow
7097 * boundaries, parents can listen without knowing about internal
7098 * structure).
7099 */
7100 emit(name, detail) {
7101 return this.dispatchEvent(
7102 new CustomEvent(name, {
7103 detail,
7104 bubbles: true,
7105 composed: true
7106 })
7107 );
7108 }
7109 // ------------------------------------------------------------------
7110 // Internals
7111 // ------------------------------------------------------------------
7112 /**
7113 * Wire every `static props` entry to a matched property getter +
7114 * setter on the element. Setting the property reflects into the
7115 * attribute (so downstream observers + CSS selectors see it);
7116 * reading the property falls back to the attribute.
7117 */
7118 _installPropAccessors() {
7119 const ctor = this.constructor;
7120 for (const prop2 of ctor.props) {
7121 if (Object.getOwnPropertyDescriptor(this, prop2)) {
7122 continue;
7123 }
7124 const attr = kebab(prop2);
7125 Object.defineProperty(this, prop2, {
7126 get: () => {
7127 if (prop2 in this._propValues) {
7128 return this._propValues[prop2];
7129 }
7130 return this.getAttribute(attr);
7131 },
7132 set: (value) => {
7133 let str2;
7134 if (value === null || value === void 0 || value === false) {
7135 str2 = null;
7136 } else if (value === true) {
7137 str2 = "";
7138 } else {
7139 str2 = String(value);
7140 }
7141 this._propValues[prop2] = str2;
7142 if (str2 === null) {
7143 this.removeAttribute(attr);
7144 } else {
7145 this.setAttribute(attr, str2);
7146 }
7147 this.requestUpdate();
7148 },
7149 enumerable: true,
7150 configurable: true
7151 });
7152 }
7153 }
7154 /**
7155 * Schedule a render on the next microtask. Multiple property
7156 * assignments in the same tick collapse into a single render.
7157 */
7158 _scheduleRender() {
7159 if (this._renderScheduled || !this.isConnected) {
7160 return;
7161 }
7162 this._renderScheduled = true;
7163 queueMicrotask(() => {
7164 this._renderScheduled = false;
7165 if (!this.isConnected) {
7166 return;
7167 }
7168 render$1(this.render(), this._renderRoot);
7169 });
7170 }
7171 /**
7172 * Mount adoptable stylesheets onto the shadow root (via
7173 * `adoptedStyleSheets`) or the light DOM (via one `<style>`
7174 * tag per def). No-op if `static styles` is empty.
7175 */
7176 _adoptStyles() {
7177 const ctor = this.constructor;
7178 if (ctor.styles.length === 0) {
7179 return;
7180 }
7181 if (ctor.shadow && this.shadowRoot) {
7182 const sheets = ctor.styles.map((s) => s.sheet).filter((s) => s !== null);
7183 this.shadowRoot.adoptedStyleSheets = sheets;
7184 if (sheets.length !== ctor.styles.length) {
7185 for (const s of ctor.styles) {
7186 if (!s.sheet) {
7187 const tag = document.createElement("style");
7188 tag.textContent = s.cssText;
7189 this.shadowRoot.appendChild(tag);
7190 }
7191 }
7192 }
7193 } else {
7194 this._adoptLightStyles(ctor);
7195 }
7196 }
7197 _adoptLightStyles(ctor) {
7198 if (_Component._lightStylesAdopted.has(ctor)) {
7199 return;
7200 }
7201 _Component._lightStylesAdopted.add(ctor);
7202 for (const s of ctor.styles) {
7203 const tag = document.createElement("style");
7204 tag.dataset.wpdUi = this.tagName.toLowerCase();
7205 tag.textContent = s.cssText;
7206 document.head.appendChild(tag);
7207 }
7208 }
7209 };
7210 _Component.props = [];
7211 _Component.styles = [];
7212 _Component.shadow = true;
7213 _Component._lightStylesAdopted = /* @__PURE__ */ new WeakSet();
7214 let Component = _Component;
7215 function defineComponent(tag, ctor) {
7216 if (customElements.get(tag)) {
7217 return;
7218 }
7219 customElements.define(tag, ctor);
7220 }
7221 function kebab(s) {
7222 return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
7223 }
7224 function camel(s) {
7225 return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
7226 }
7227 const SUPPORTS_CONSTRUCTABLE_SHEETS = (() => {
7228 try {
7229 const s = new CSSStyleSheet();
7230 return typeof s.replaceSync === "function";
7231 } catch {
7232 return false;
7233 }
7234 })();
7235 function css(strings, ...values) {
7236 let text = strings[0];
7237 for (let i = 1; i < strings.length; i++) {
7238 const v = values[i - 1];
7239 if (typeof v === "string" || typeof v === "number") {
7240 text += String(v);
7241 } else if (v && v.__wpdCss) {
7242 text += v.cssText;
7243 } else {
7244 throw new TypeError(
7245 "[wpd-ui] css`` interpolations must be strings, numbers, or other css`` results. Got: " + typeof v
7246 );
7247 }
7248 text += strings[i];
7249 }
7250 if (SUPPORTS_CONSTRUCTABLE_SHEETS) {
7251 const sheet = new CSSStyleSheet();
7252 sheet.replaceSync(text);
7253 return { __wpdCss: true, sheet, cssText: text };
7254 }
7255 return { __wpdCss: true, sheet: null, cssText: text };
7256 }
7257 function computeAutoId(element) {
7258 const parts = [];
7259 const tabs = [];
7260 let windowId = null;
7261 let node = element.parentElement;
7262 while (node) {
7263 if (node === document.body || node === document.documentElement) {
7264 break;
7265 }
7266 const id = node.id || "";
7267 if (id.startsWith("wp-window-")) {
7268 windowId = id.slice("wp-window-".length);
7269 break;
7270 }
7271 if (node.tagName.toLowerCase() === "wpd-tabpanel") {
7272 const forValue = node.getAttribute("for");
7273 if (forValue) {
7274 tabs.unshift(forValue);
7275 }
7276 }
7277 node = node.parentElement;
7278 }
7279 if (windowId) {
7280 parts.push(slugify(windowId));
7281 }
7282 for (const tab of tabs) {
7283 parts.push("tab-" + slugify(tab));
7284 }
7285 const label = element.getAttribute("label");
7286 if (label) {
7287 parts.push(slugify(label));
7288 }
7289 if (parts.length === 0) {
7290 return "wpd-unnamed";
7291 }
7292 return "wpd-" + parts.filter((p) => p !== "").join("-");
7293 }
7294 function slugify(s) {
7295 return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7296 }
7297 function ensureAutoId(element) {
7298 if (element.id) {
7299 return element.id;
7300 }
7301 const id = computeAutoId(element);
7302 element.id = id;
7303 return id;
7304 }
7305 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 )}`;
7306 const _WpdConfirmDialog = class _WpdConfirmDialog extends Component {
7307 constructor() {
7308 super(...arguments);
7309 this._onKey = (e) => {
7310 if (e.key === "Escape") {
7311 e.preventDefault();
7312 this._cancel();
7313 }
7314 if (e.key === "Enter" && !e.isComposing) {
7315 e.preventDefault();
7316 this._confirm();
7317 }
7318 };
7319 this._onBackdrop = (e) => {
7320 const path = e.composedPath();
7321 const original = path.length > 0 ? path[0] : e.target;
7322 if (original === this) {
7323 this._cancel();
7324 }
7325 };
7326 this._confirm = () => {
7327 this.emit("wpd-confirm", { confirmed: true });
7328 this.removeAttribute("open");
7329 };
7330 this._cancel = () => {
7331 this.emit("wpd-cancel", { confirmed: false });
7332 this.removeAttribute("open");
7333 };
7334 }
7335 connectedCallback() {
7336 super.connectedCallback();
7337 this.setAttribute("role", "dialog");
7338 this.setAttribute("aria-modal", "true");
7339 this.addEventListener("keydown", this._onKey);
7340 this.addEventListener("click", this._onBackdrop);
7341 }
7342 disconnectedCallback() {
7343 this.removeEventListener("keydown", this._onKey);
7344 this.removeEventListener("click", this._onBackdrop);
7345 }
7346 render() {
7347 const title = this.title ?? "";
7348 const message = this.message ?? "";
7349 const confirmLabel = this["confirm-label"] || "Confirm";
7350 const cancelLabel = this["cancel-label"] || "Cancel";
7351 const isDanger = this.hasAttribute("danger");
7352 const hideCancel = this.hasAttribute("hide-cancel");
7353 const isDismissable = this.hasAttribute("dismissable");
7354 return html`
7355 <div class="dialog" tabindex="-1">
7356 ${isDismissable ? html`<button
7357 type="button"
7358 class="close"
7359 aria-label="Close"
7360 @click=${() => this._cancel()}
7361 >&times;</button>` : html``}
7362 ${title ? html`<h2 class="title">${title}</h2>` : html``}
7363 ${message ? html`<p class="message">${message}</p>` : html``}
7364 <div class="actions">
7365 ${hideCancel ? html`` : html`<button
7366 type="button"
7367 class="btn btn--secondary"
7368 @click=${() => this._cancel()}
7369 >
7370 ${cancelLabel}
7371 </button>`}
7372 <button
7373 type="button"
7374 class="btn ${isDanger ? "btn--danger" : "btn--primary"}"
7375 @click=${() => this._confirm()}
7376 >
7377 ${confirmLabel}
7378 </button>
7379 </div>
7380 </div>
7381 `;
7382 }
7383 };
7384 _WpdConfirmDialog.props = [
7385 "open",
7386 "title",
7387 "message",
7388 "confirm-label",
7389 "cancel-label",
7390 "danger",
7391 "hide-cancel",
7392 "dismissable"
7393 ];
7394 _WpdConfirmDialog.styles = [dialogStyles];
7395 _WpdConfirmDialog.help = {
7396 title: "Confirm dialog",
7397 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.",
7398 status: "experimental",
7399 since: "0.9.0",
7400 props: [
7401 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
7402 { name: "title", type: "string", description: "Heading shown at the top." },
7403 { name: "message", type: "string", description: "Body copy. Newlines preserved." },
7404 { name: "confirm-label", type: "string", default: "Confirm", description: "Confirm-button label." },
7405 { name: "cancel-label", type: "string", default: "Cancel", description: "Cancel-button label." },
7406 { name: "danger", type: "boolean attribute", description: "Renders the confirm button red." },
7407 { 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." },
7408 { name: "dismissable", type: "boolean attribute", description: "Renders an X close button in the top-right corner. Click emits `wpd-cancel`." }
7409 ],
7410 events: [
7411 {
7412 name: "wpd-confirm",
7413 description: "Fires on confirm. Detail: `{ confirmed: true }`."
7414 },
7415 {
7416 name: "wpd-cancel",
7417 description: "Fires on cancel (Cancel button, Escape, backdrop click). Detail: `{ confirmed: false }`."
7418 }
7419 ]
7420 };
7421 let WpdConfirmDialog = _WpdConfirmDialog;
7422 defineComponent("wpd-confirm-dialog", WpdConfirmDialog);
7423 function wpdConfirm$1(options) {
7424 return new Promise((resolve2) => {
7425 const dialog2 = document.createElement("wpd-confirm-dialog");
7426 dialog2.setAttribute("open", "");
7427 if (options.title) {
7428 dialog2.setAttribute("title", options.title);
7429 }
7430 dialog2.setAttribute("message", options.message);
7431 if (options.confirmLabel) {
7432 dialog2.setAttribute("confirm-label", options.confirmLabel);
7433 }
7434 if (options.cancelLabel) {
7435 dialog2.setAttribute("cancel-label", options.cancelLabel);
7436 }
7437 if (options.danger) {
7438 dialog2.setAttribute("danger", "");
7439 }
7440 if (options.hideCancel) {
7441 dialog2.setAttribute("hide-cancel", "");
7442 }
7443 if (options.dismissable) {
7444 dialog2.setAttribute("dismissable", "");
7445 }
7446 const cleanup = (ok) => {
7447 dialog2.remove();
7448 resolve2(ok);
7449 };
7450 dialog2.addEventListener("wpd-confirm", () => cleanup(true));
7451 dialog2.addEventListener("wpd-cancel", () => cleanup(false));
7452 document.body.appendChild(dialog2);
7453 const inner = dialog2.shadowRoot?.querySelector(".dialog");
7454 (inner ?? dialog2).focus?.();
7455 });
7456 }
7457 const FALLBACK_BASE = "http://localhost/";
7458 function joinRestUrl(restRoot2, path) {
7459 const base = typeof window !== "undefined" && window.location ? window.location.href : FALLBACK_BASE;
7460 const url = new URL(restRoot2, base);
7461 const trimmed = path.replace(/^\/+/, "");
7462 const queryAt = trimmed.indexOf("?");
7463 const route = queryAt === -1 ? trimmed : trimmed.slice(0, queryAt);
7464 const extraQuery = queryAt === -1 ? "" : trimmed.slice(queryAt + 1);
7465 if (url.searchParams.has("rest_route")) {
7466 const existing = url.searchParams.get("rest_route") ?? "/";
7467 const prefix = existing.endsWith("/") ? existing : existing + "/";
7468 url.searchParams.set("rest_route", prefix + route);
7469 } else {
7470 const pathname = url.pathname.endsWith("/") ? url.pathname : url.pathname + "/";
7471 url.pathname = pathname + route;
7472 }
7473 if (extraQuery) {
7474 const extras = new URLSearchParams(extraQuery);
7475 extras.forEach((value, key) => {
7476 url.searchParams.append(key, value);
7477 });
7478 }
7479 return url.toString();
7480 }
7481 function getApi() {
7482 const w = window;
7483 return w.wp?.desktop ?? null;
7484 }
7485 let activeMenu$3 = null;
7486 function closeMenu$1() {
7487 if (activeMenu$3) {
7488 activeMenu$3.remove();
7489 activeMenu$3 = null;
7490 }
7491 }
7492 function writeVisibility(canonicalId, placement) {
7493 const api = getApi();
7494 if (!api?.getOsSettings || !api?.updateOsSettings) {
7495 return;
7496 }
7497 const snap = api.getOsSettings();
7498 const next = { ...snap.itemVisibility };
7499 next[canonicalId] = placement;
7500 api.updateOsSettings({ itemVisibility: next });
7501 }
7502 function railFromId(id, surface) {
7503 if (id.startsWith("dock:")) {
7504 return "dock";
7505 }
7506 if (id.startsWith("desktop:")) {
7507 return "desktop";
7508 }
7509 return surface;
7510 }
7511 function computeHideTarget(canonicalId, nativeRail, hideSurface, visibility) {
7512 const current = resolvePlacement(canonicalId, nativeRail, visibility);
7513 if (current === "both") {
7514 return hideSurface === "dock" ? "desktop" : "dock";
7515 }
7516 return "hidden";
7517 }
7518 let openGeneration$2 = 0;
7519 function openItemVisibilityMenu(opts) {
7520 closeMenu$1();
7521 const myGen = ++openGeneration$2;
7522 openWithShellOverlays(
7523 () => myGen === openGeneration$2,
7524 () => openItemVisibilityMenuImmediate(opts)
7525 );
7526 }
7527 function openItemVisibilityMenuImmediate(opts) {
7528 closeMenu$1();
7529 const canonical = canonicalItemId(opts.id);
7530 const nativeRail = railFromId(opts.id, opts.surface);
7531 const currentPlacement2 = resolvePlacement(
7532 canonical,
7533 nativeRail,
7534 getApi()?.getOsSettings?.().itemVisibility ?? {}
7535 );
7536 const options = [];
7537 if (opts.surface === "dock") {
7538 options.push({
7539 id: "hide-from-dock",
7540 label: __("Hide from dock"),
7541 icon: "dashicons-hidden",
7542 onPick: () => writeVisibility(
7543 canonical,
7544 computeHideTarget(
7545 canonical,
7546 nativeRail,
7547 "dock",
7548 getApi()?.getOsSettings?.().itemVisibility ?? {}
7549 )
7550 )
7551 });
7552 if (currentPlacement2 !== "both") {
7553 options.push({
7554 id: "show-on-desktop-too",
7555 label: __("Also show on desktop"),
7556 icon: "dashicons-desktop",
7557 onPick: () => writeVisibility(canonical, "both")
7558 });
7559 }
7560 } else {
7561 options.push({
7562 id: "hide-from-desktop",
7563 label: __("Hide from desktop"),
7564 icon: "dashicons-hidden",
7565 onPick: () => writeVisibility(
7566 canonical,
7567 computeHideTarget(
7568 canonical,
7569 nativeRail,
7570 "desktop",
7571 getApi()?.getOsSettings?.().itemVisibility ?? {}
7572 )
7573 )
7574 });
7575 if (currentPlacement2 !== "both") {
7576 options.push({
7577 id: "show-on-dock-too",
7578 label: __("Also show on dock"),
7579 icon: "dashicons-menu",
7580 onPick: () => writeVisibility(canonical, "both")
7581 });
7582 }
7583 }
7584 options.push({
7585 id: "hide-everywhere",
7586 label: __("Hide everywhere"),
7587 icon: "dashicons-no",
7588 danger: true,
7589 onPick: () => writeVisibility(canonical, "hidden")
7590 });
7591 options.push({
7592 id: "open-settings",
7593 label: __("Apps & Icons settings…"),
7594 icon: "dashicons-admin-generic",
7595 onPick: () => {
7596 const api = getApi();
7597 api?.openOsSettings?.({ tabId: "apps-icons" });
7598 }
7599 });
7600 if (opts.pluginFile) {
7601 const pluginFile = opts.pluginFile;
7602 const pluginLabel = opts.pluginName || opts.title;
7603 options.push({ kind: "separator" });
7604 options.push({
7605 id: "deactivate-plugin",
7606 // translators: %s is the owning plugin's display name.
7607 label: sprintf(__("Deactivate %s…"), pluginLabel),
7608 icon: "dashicons-trash",
7609 danger: true,
7610 onPick: () => {
7611 void confirmAndDeactivatePlugin(pluginFile, pluginLabel);
7612 }
7613 });
7614 }
7615 const menu = document.createElement("wpd-context-menu");
7616 menu.setAttribute("open", "");
7617 menu.classList.add("desktop-mode-item-visibility-menu");
7618 menu.dataset.itemId = opts.id;
7619 menu.style.position = "fixed";
7620 menu.style.left = "-9999px";
7621 menu.style.top = "-9999px";
7622 menu.style.visibility = "hidden";
7623 menu.style.zIndex = "1000000";
7624 const byKey = /* @__PURE__ */ new Map();
7625 for (const opt of options) {
7626 if (opt.kind === "separator") {
7627 const hr = document.createElement("hr");
7628 hr.style.cssText = "border: 0; border-top: 1px solid var( --wpd-context-menu-separator-color, rgba(255,255,255,0.12) ); margin: 4px 6px;";
7629 menu.appendChild(hr);
7630 continue;
7631 }
7632 byKey.set(opt.id, opt);
7633 const node = document.createElement("wpd-context-menu-option");
7634 node.dataset.menuItemId = opt.id;
7635 node.setAttribute("value", opt.id);
7636 if (opt.icon) {
7637 node.setAttribute("icon", opt.icon);
7638 }
7639 if (opt.danger) {
7640 node.setAttribute("danger", "");
7641 }
7642 node.textContent = opt.label;
7643 menu.appendChild(node);
7644 }
7645 menu.addEventListener("wpd-context-menu-pick", (e) => {
7646 const detail = e.detail;
7647 const key = detail?.id || detail?.value || "";
7648 const opt = byKey.get(key);
7649 closeMenu$1();
7650 try {
7651 opt?.onPick();
7652 } catch {
7653 }
7654 });
7655 document.body.appendChild(menu);
7656 activeMenu$3 = menu;
7657 const positionMenu = () => {
7658 if (menu !== activeMenu$3) {
7659 return;
7660 }
7661 const rect = menu.getBoundingClientRect();
7662 const margin = 8;
7663 let left = opts.x;
7664 let top;
7665 if (opts.surface === "dock") {
7666 top = Math.max(margin, opts.y - rect.height - margin);
7667 } else {
7668 top = opts.y;
7669 if (top + rect.height + margin > window.innerHeight) {
7670 top = Math.max(margin, opts.y - rect.height);
7671 }
7672 }
7673 if (left + rect.width + margin > window.innerWidth) {
7674 left = Math.max(margin, opts.x - rect.width);
7675 }
7676 menu.style.left = `${left}px`;
7677 menu.style.top = `${top}px`;
7678 menu.style.visibility = "";
7679 };
7680 requestAnimationFrame(positionMenu);
7681 const onOutside = (ev) => {
7682 if (!activeMenu$3) {
7683 return;
7684 }
7685 if (!activeMenu$3.contains(ev.target)) {
7686 closeMenu$1();
7687 document.removeEventListener("mousedown", onOutside, true);
7688 document.removeEventListener("keydown", onKey, true);
7689 }
7690 };
7691 const onKey = (ev) => {
7692 if (ev.key === "Escape") {
7693 closeMenu$1();
7694 document.removeEventListener("mousedown", onOutside, true);
7695 document.removeEventListener("keydown", onKey, true);
7696 }
7697 };
7698 document.addEventListener("mousedown", onOutside, true);
7699 document.addEventListener("keydown", onKey, true);
7700 }
7701 async function confirmAndDeactivatePlugin(pluginFile, title) {
7702 const confirmed = await wpdConfirm$1({
7703 /* translators: %s: plugin title. */
7704 title: sprintf(__("Deactivate %s?"), title),
7705 message: __(
7706 "This plugin will stop running on the site. You can re-activate it later from the Plugins screen."
7707 ),
7708 confirmLabel: __("Deactivate"),
7709 cancelLabel: __("Cancel"),
7710 danger: true
7711 });
7712 if (!confirmed) {
7713 return;
7714 }
7715 const cfg = window.desktopModeConfig ?? {};
7716 const restRoot2 = typeof cfg.restRoot === "string" && cfg.restRoot ? cfg.restRoot : `${window.location.origin}/wp-json/`;
7717 const restNonce = typeof cfg.restNonce === "string" && cfg.restNonce ? cfg.restNonce : "";
7718 const stripped = pluginFile.endsWith(".php") ? pluginFile.slice(0, -4) : pluginFile;
7719 const encoded = stripped.split("/").map(encodeURIComponent).join("/");
7720 const url = joinRestUrl(restRoot2, `wp/v2/plugins/${encoded}`);
7721 try {
7722 const res = await trackedFetch$1(
7723 url,
7724 {
7725 method: "PUT",
7726 headers: {
7727 "Content-Type": "application/json",
7728 "X-WP-Nonce": restNonce
7729 },
7730 body: JSON.stringify({ status: "inactive" }),
7731 credentials: "same-origin"
7732 },
7733 { source: "desktop-mode/dock-deactivate-plugin" }
7734 );
7735 if (!res.ok) {
7736 throw new Error(`HTTP ${res.status}`);
7737 }
7738 } catch (err) {
7739 showToast({
7740 message: sprintf(
7741 /* translators: %s: plugin title. */
7742 __("Could not deactivate %s."),
7743 title
7744 ),
7745 duration: 4e3
7746 });
7747 console.error("[desktop-mode] deactivate plugin failed", err);
7748 return;
7749 }
7750 const closedTitles = closeWindowsForPlugin(pluginFile);
7751 const deactivatedMsg = closedTitles.length > 0 ? sprintf(
7752 /* translators: 1: plugin title. 2: number of windows that were closed. */
7753 __("%1$s deactivated. Closed %2$d window(s)."),
7754 title,
7755 closedTitles.length
7756 ) : sprintf(
7757 /* translators: %s: plugin title. */
7758 __("%s deactivated."),
7759 title
7760 );
7761 showToast({ message: deactivatedMsg, duration: 3e3 });
7762 const w = window;
7763 w.wp?.desktop?.refreshMenu?.();
7764 }
7765 function closeWindowsForPlugin(pluginFile) {
7766 const api = window.wp?.desktop;
7767 if (!api?.windowManager?.getAll) {
7768 return [];
7769 }
7770 const items = api.getMenuItems?.() ?? [];
7771 const owned = items.filter((i) => i.pluginFile === pluginFile);
7772 if (owned.length === 0) {
7773 return [];
7774 }
7775 const ownedKeys = /* @__PURE__ */ new Set();
7776 for (const item of owned) {
7777 ownedKeys.add(item.id);
7778 if (api.deriveWindowId) {
7779 ownedKeys.add(api.deriveWindowId(item.url));
7780 }
7781 }
7782 const toClose = /* @__PURE__ */ new Map();
7783 const windows = api.windowManager.getAll() ?? [];
7784 const derive = api.deriveWindowId;
7785 for (const w of windows) {
7786 if (ownedKeys.has(w.id)) {
7787 toClose.set(w.id, w);
7788 continue;
7789 }
7790 if (w.config?.baseId && ownedKeys.has(w.config.baseId)) {
7791 toClose.set(w.id, w);
7792 continue;
7793 }
7794 if (derive && w.config?.url) {
7795 const derivedFromConfig = derive(w.config.url);
7796 if (ownedKeys.has(derivedFromConfig)) {
7797 toClose.set(w.id, w);
7798 continue;
7799 }
7800 }
7801 if (derive && w.iframe) {
7802 let liveUrl = "";
7803 try {
7804 liveUrl = w.iframe.src || "";
7805 } catch {
7806 }
7807 if (liveUrl) {
7808 const derivedFromLive = derive(liveUrl);
7809 if (ownedKeys.has(derivedFromLive)) {
7810 toClose.set(w.id, w);
7811 }
7812 }
7813 }
7814 }
7815 const titles = [];
7816 for (const w of toClose.values()) {
7817 titles.push(w.config?.title ?? w.id);
7818 try {
7819 w.close();
7820 } catch {
7821 }
7822 }
7823 return titles;
7824 }
7825 const _Dock = class _Dock {
7826 constructor(container, windowManager, items, adminUrl, orientation = "left") {
7827 this.itemElements = /* @__PURE__ */ new Map();
7828 this.systemItems = [];
7829 this.systemItemElements = /* @__PURE__ */ new Map();
7830 this.systemSeparator = null;
7831 this.badgeOverrides = /* @__PURE__ */ new Map();
7832 this.attentionTimers = /* @__PURE__ */ new Map();
7833 this.peekTeardowns = /* @__PURE__ */ new Map();
7834 this.boundRefresh = () => void 0;
7835 this.container = container;
7836 this.windowManager = windowManager;
7837 this.items = items;
7838 this.adminUrl = adminUrl;
7839 this.orientation = orientation;
7840 this.rail = orientation === "bottom" ? "taskbar" : "dock";
7841 this.hooksNamespace = `desktop-mode/dock/${++_Dock.instanceCounter}`;
7842 this.container.setAttribute(
7843 "data-desktop-mode-dock-placement",
7844 orientation
7845 );
7846 const scroll = document.createElement("div");
7847 scroll.className = "desktop-mode-dock__scroll";
7848 const pinned = document.createElement("div");
7849 pinned.className = "desktop-mode-dock__pinned";
7850 container.appendChild(scroll);
7851 container.appendChild(pinned);
7852 this.itemHost = scroll;
7853 this.systemHost = pinned;
7854 this.tooltip = document.createElement("div");
7855 this.tooltip.className = "desktop-mode-dock__tooltip";
7856 this.tooltip.setAttribute("role", "tooltip");
7857 if (orientation === "bottom") {
7858 this.tooltip.classList.add("desktop-mode-dock__tooltip--above");
7859 } else if (orientation === "right") {
7860 this.tooltip.classList.add("desktop-mode-dock__tooltip--before");
7861 } else {
7862 this.tooltip.classList.add("desktop-mode-dock__tooltip--after");
7863 }
7864 document.body.appendChild(this.tooltip);
7865 this.render();
7866 this.bindWindowEvents();
7867 }
7868 /**
7869 * Build the base context object every dock decoration hook
7870 * receives. Read from `this` so a single subscriber can
7871 * disambiguate two coexisting rails by `dockId`.
7872 */
7873 buildHookContextBase() {
7874 return {
7875 rail: this.rail,
7876 orientation: this.orientation,
7877 dockId: this.container.id,
7878 container: this.container
7879 };
7880 }
7881 /**
7882 * Replace the menu-derived tile list with a fresh one, preserving
7883 * any JS-registered system tiles. Used by the live menu-refresh
7884 * path: after a plugin is activated or deactivated, the chromeless
7885 * bridge postMessages a fresh payload built from real admin
7886 * context, and the shell calls this so the dock repaints without
7887 * a tab reload.
7888 *
7889 * Old menu tiles are removed from both the DOM and the lookup
7890 * map; new tiles are inserted before the system separator (or
7891 * appended at the end if none exists yet), so the menu-items →
7892 * hairline → system-items ordering stays intact. Active-state
7893 * classes are re-computed once the new tiles are in place so
7894 * window indicators survive the swap.
7895 *
7896 * @param items New DockItem list. Pass `[]` to clear everything
7897 * menu-derived.
7898 */
7899 /**
7900 * Update the dock's orientation. Writes the new value to the
7901 * dock element's `data-desktop-mode-dock-placement` attribute (CSS
7902 * keys off it for layout) and keeps the tooltip anchor in sync.
7903 *
7904 * In practice, the layout dispatcher in `desktop.ts` rebuilds the
7905 * dock(s) from scratch on a layout change rather than re-orienting
7906 * a live instance — but this stays correct in case any caller
7907 * wants to flip orientation without the rebuild.
7908 */
7909 setOrientation(orientation) {
7910 if (this.orientation === orientation) {
7911 return;
7912 }
7913 this.orientation = orientation;
7914 this.container.setAttribute(
7915 "data-desktop-mode-dock-placement",
7916 orientation
7917 );
7918 this.tooltip.classList.remove(
7919 "desktop-mode-dock__tooltip--above",
7920 "desktop-mode-dock__tooltip--before",
7921 "desktop-mode-dock__tooltip--after"
7922 );
7923 if (orientation === "bottom") {
7924 this.tooltip.classList.add("desktop-mode-dock__tooltip--above");
7925 } else if (orientation === "right") {
7926 this.tooltip.classList.add("desktop-mode-dock__tooltip--before");
7927 } else {
7928 this.tooltip.classList.add("desktop-mode-dock__tooltip--after");
7929 }
7930 }
7931 replaceItems(items) {
7932 for (const itemId of this.itemElements.keys()) {
7933 const teardown = this.peekTeardowns.get(itemId);
7934 if (teardown) {
7935 teardown();
7936 this.peekTeardowns.delete(itemId);
7937 }
7938 }
7939 for (const el of this.itemElements.values()) {
7940 el.remove();
7941 }
7942 this.itemHost.querySelectorAll(
7943 ".desktop-mode-dock__separator--group"
7944 ).forEach((el) => el.remove());
7945 this.itemElements.clear();
7946 this.items = items;
7947 const base = this.buildHookContextBase();
7948 doAction(HOOKS.DOCK_BEFORE_RENDER, {
7949 ...base,
7950 items,
7951 tileElements: this.itemElements
7952 });
7953 let insertedGroupSeparator = false;
7954 let tilesInsertedThisPass = 0;
7955 for (const item of items) {
7956 if (!insertedGroupSeparator && item.isCore === false) {
7957 if (tilesInsertedThisPass > 0) {
7958 const sep = document.createElement("div");
7959 sep.className = "desktop-mode-dock__separator desktop-mode-dock__separator--group";
7960 sep.setAttribute("aria-hidden", "true");
7961 this.itemHost.appendChild(sep);
7962 }
7963 insertedGroupSeparator = true;
7964 }
7965 const btn = this.createItemButton(item);
7966 this.itemElements.set(item.id, btn);
7967 this.itemHost.appendChild(btn);
7968 tilesInsertedThisPass++;
7969 const override = this.badgeOverrides.get(item.id);
7970 if (override !== void 0) {
7971 const primary = btn.querySelector(
7972 ".desktop-mode-dock__item-primary"
7973 );
7974 _applyBadgeNode(primary ?? btn, override);
7975 }
7976 doAction(HOOKS.DOCK_TILE_RENDERED, {
7977 ...base,
7978 item,
7979 isSystem: false,
7980 el: btn
7981 });
7982 }
7983 this.updateActiveStates();
7984 doAction(HOOKS.DOCK_AFTER_RENDER, {
7985 ...base,
7986 items,
7987 tileElements: this.itemElements
7988 });
7989 }
7990 /**
7991 * True when the rail currently has ANY renderable tile —
7992 * either a menu-derived item or a JS-registered system item.
7993 * Lets callers (the shell's live-refresh path) decide whether
7994 * to hide the whole rail without having to peek into two
7995 * internal maps. "System tiles keep the rail alive even when
7996 * menu items are empty" is the user-visible contract we enforce.
7997 */
7998 hasItems() {
7999 return this.itemElements.size > 0 || this.systemItemElements.size > 0;
8000 }
8001 /**
8002 * Remove a previously-registered system item. Used by the
8003 * server-driven native-window sync path — when a plugin is
8004 * deactivated, its native-window entry disappears from the
8005 * server's payload and the shell calls this to pull the tile
8006 * back off the rail without a reload.
8007 *
8008 * Idempotent: an unknown id is a silent no-op. The system
8009 * separator is kept in place as long as at least one system
8010 * item remains; removing the last system item also strips the
8011 * separator so the rail doesn't dangle a divider under nothing.
8012 */
8013 removeSystemItem(id) {
8014 const tile2 = this.systemItemElements.get(id);
8015 if (!tile2) {
8016 return;
8017 }
8018 tile2.remove();
8019 this.systemItemElements.delete(id);
8020 this.systemItems = this.systemItems.filter((s) => s.id !== id);
8021 this.badgeOverrides.delete(id);
8022 if (this.systemItemElements.size === 0 && this.systemSeparator) {
8023 this.systemSeparator.remove();
8024 this.systemSeparator = null;
8025 }
8026 doAction(HOOKS.DOCK_ITEM_REMOVED, { id, placement: this.rail });
8027 }
8028 /**
8029 * Set the badge count on a tile. Live-updates without a full
8030 * dock re-render — the existing tile's badge node is mutated in
8031 * place (or created if missing). Pass `0` to remove the badge.
8032 *
8033 * Resolves the tile in id order: menu items (`data-menu-slug`)
8034 * first, then system items (`data-system-id`), so callers can
8035 * use the same id surface regardless of which rail the tile
8036 * happens to live on.
8037 *
8038 * Idempotent: applying the same count is a no-op (no DOM mutation).
8039 *
8040 * @since 0.6.0
8041 *
8042 * @param itemId Tile id (menu slug for admin pages, system id
8043 * for `appendSystemItem` / `registerSystemTile`).
8044 * @param count Non-negative integer. `>99` renders as `99+`.
8045 */
8046 setBadge(itemId, count) {
8047 const tile2 = this._resolveTileElement(itemId);
8048 if (!tile2) {
8049 return;
8050 }
8051 const safe = Math.max(0, Math.floor(Number(count) || 0));
8052 if (safe === 0) {
8053 this.badgeOverrides.delete(itemId);
8054 } else {
8055 this.badgeOverrides.set(itemId, safe);
8056 }
8057 const primary = tile2.querySelector(
8058 ".desktop-mode-dock__item-primary"
8059 );
8060 _applyBadgeNode(primary ?? tile2, safe);
8061 activity.publish("desktop-mode/badge-changed", {
8062 itemId,
8063 count: safe,
8064 rail: this.rail
8065 });
8066 }
8067 /**
8068 * Clear the badge on a tile. Equivalent to `setBadge( id, 0 )`.
8069 *
8070 * @since 0.6.0
8071 */
8072 clearBadge(itemId) {
8073 this.setBadge(itemId, 0);
8074 }
8075 /**
8076 * Apply or clear an attention animation on a tile.
8077 *
8078 * - `'pulse'` — soft halo + scale, ~1.4 s loop. Default.
8079 * - `'shake'` — short horizontal jiggle.
8080 * - `'bounce'` — vertical bob, attention-grabbing.
8081 * - `null` — clear any active attention.
8082 *
8083 * Animations are gated on `prefers-reduced-motion: no-preference`;
8084 * the reduced-motion fallback shows a static accent ring for the
8085 * same duration so the affordance still works. `durationMs` of
8086 * `0` keeps the attention until the next call clears it.
8087 *
8088 * @since 0.6.0
8089 *
8090 * @param itemId Tile id.
8091 * @param mode Animation mode or `null` to clear.
8092 * @param opts Optional duration / intensity overrides.
8093 */
8094 setAttention(itemId, mode, opts = {}) {
8095 const tile2 = this._resolveTileElement(itemId);
8096 if (!tile2) {
8097 return;
8098 }
8099 const pending2 = this.attentionTimers.get(itemId);
8100 if (pending2 !== void 0) {
8101 window.clearTimeout(pending2);
8102 this.attentionTimers.delete(itemId);
8103 }
8104 tile2.classList.remove(
8105 "desktop-mode-dock__item--attention-pulse",
8106 "desktop-mode-dock__item--attention-shake",
8107 "desktop-mode-dock__item--attention-bounce",
8108 "desktop-mode-dock__item--intensity-subtle",
8109 "desktop-mode-dock__item--intensity-normal",
8110 "desktop-mode-dock__item--intensity-strong"
8111 );
8112 if (mode === null) {
8113 return;
8114 }
8115 tile2.classList.add(`desktop-mode-dock__item--attention-${mode}`);
8116 const intensity = opts.intensity ?? "normal";
8117 tile2.classList.add(`desktop-mode-dock__item--intensity-${intensity}`);
8118 const duration = opts.durationMs ?? 4e3;
8119 if (duration > 0) {
8120 const handle = window.setTimeout(() => {
8121 this.attentionTimers.delete(itemId);
8122 this.setAttention(itemId, null);
8123 }, duration);
8124 this.attentionTimers.set(itemId, handle);
8125 }
8126 }
8127 /**
8128 * Resolve a tile element by id — checks menu items first
8129 * (`data-menu-slug`), then system items (`data-system-id`). Used
8130 * by `setBadge` / `setAttention` so callers can reach either rail
8131 * with one id surface.
8132 */
8133 _resolveTileElement(itemId) {
8134 return this.itemElements.get(itemId) ?? this.systemItemElements.get(itemId) ?? null;
8135 }
8136 /**
8137 * Append a JS-registered system item to the dock.
8138 *
8139 * System items render after the menu-derived items, separated by a
8140 * hairline divider. Use for shell affordances that don't live in
8141 * the admin menu: OS Settings today, Jorvy and desktop widgets
8142 * later. Callers supply their own `onOpen` — the dock doesn't
8143 * assume the item opens a window at all.
8144 */
8145 appendSystemItem(item) {
8146 this.systemItems.push(item);
8147 if (!this.systemSeparator) {
8148 this.systemSeparator = document.createElement("div");
8149 this.systemSeparator.className = "desktop-mode-dock__separator";
8150 this.systemSeparator.setAttribute("aria-hidden", "true");
8151 this.systemHost.appendChild(this.systemSeparator);
8152 }
8153 const tile2 = this.createSystemItemButton(item);
8154 this.systemItemElements.set(item.id, tile2);
8155 this.systemHost.appendChild(tile2);
8156 this.updateActiveStates();
8157 doAction(HOOKS.DOCK_TILE_RENDERED, {
8158 ...this.buildHookContextBase(),
8159 item,
8160 isSystem: true,
8161 el: tile2
8162 });
8163 }
8164 /**
8165 * Render the dock contents.
8166 *
8167 * Items are ordered server-side with core WordPress menus first and
8168 * plugin-contributed menus after. We insert a `--group` separator
8169 * at the first core→plugin transition so the two clusters read as
8170 * distinct groups of tiles — "default apps" and "installed apps"
8171 * in macOS-dock parlance. The separator is skipped when the menu
8172 * contains only one kind (no plugin menus, or a theme's filter
8173 * reordered everything into one class).
8174 */
8175 render() {
8176 if (_Dock.activeDragReset) {
8177 const prev = _Dock.activeDragReset;
8178 _Dock.activeDragReset = null;
8179 prev();
8180 }
8181 for (const teardown of this.peekTeardowns.values()) {
8182 teardown();
8183 }
8184 this.peekTeardowns.clear();
8185 this.itemHost.innerHTML = "";
8186 const base = this.buildHookContextBase();
8187 doAction(HOOKS.DOCK_BEFORE_RENDER, {
8188 ...base,
8189 items: this.items,
8190 tileElements: this.itemElements
8191 });
8192 let insertedGroupSeparator = false;
8193 for (const item of this.items) {
8194 if (!insertedGroupSeparator && item.isCore === false) {
8195 if (this.itemHost.childElementCount > 0) {
8196 const sep = document.createElement("div");
8197 sep.className = "desktop-mode-dock__separator desktop-mode-dock__separator--group";
8198 sep.setAttribute("aria-hidden", "true");
8199 this.itemHost.appendChild(sep);
8200 }
8201 insertedGroupSeparator = true;
8202 }
8203 const btn = this.createItemButton(item);
8204 this.itemElements.set(item.id, btn);
8205 this.itemHost.appendChild(btn);
8206 doAction(HOOKS.DOCK_TILE_RENDERED, {
8207 ...base,
8208 item,
8209 isSystem: false,
8210 el: btn
8211 });
8212 }
8213 doAction(HOOKS.DOCK_AFTER_RENDER, {
8214 ...base,
8215 items: this.items,
8216 tileElements: this.itemElements
8217 });
8218 }
8219 /**
8220 * Create a tile for a JS-registered system item. Structurally simpler
8221 * than a menu tile — no submenu, no multi-instance rail, no badge —
8222 * but uses the same base classes so the hover / focus / active
8223 * styling is shared.
8224 */
8225 createSystemItemButton(item) {
8226 const ctx = {
8227 ...this.buildHookContextBase(),
8228 item,
8229 isSystem: true
8230 };
8231 const tile2 = document.createElement("div");
8232 const baseClasses = [
8233 "desktop-mode-dock__item",
8234 "desktop-mode-dock__item--system"
8235 ];
8236 const filteredClasses = applyFilters(
8237 HOOKS.DOCK_TILE_CLASS,
8238 baseClasses,
8239 ctx
8240 );
8241 tile2.className = filteredClasses.join(" ");
8242 tile2.dataset.systemId = item.id;
8243 const primary = document.createElement("button");
8244 primary.className = "desktop-mode-dock__item-primary";
8245 primary.setAttribute("type", "button");
8246 primary.setAttribute("aria-label", item.title);
8247 primary.appendChild(this.resolveIcon(item.icon, item.title));
8248 primary.addEventListener("click", () => item.onOpen());
8249 tile2.appendChild(primary);
8250 this.bindTooltipFiltered(tile2, item.title, ctx);
8251 const teardown = attachDockPeek({
8252 tile: tile2,
8253 item: {
8254 id: item.id,
8255 title: item.title,
8256 icon: item.icon,
8257 url: ""
8258 },
8259 // System tiles target a single native-window id; that id
8260 // is also the baseId the manager stores duplicates under
8261 // when the user opens additional instances via the Ghost
8262 // Card. `getAllByBaseId` returns `[]` / `[one]` for the
8263 // singleton cases and the full set when a multi-capable
8264 // system tile (`multi: true`) has been duplicated.
8265 getInstances: () => this.windowManager.getAllByBaseIdOnActiveDesktop(item.id),
8266 enableGhost: !!item.multi,
8267 windowManager: this.windowManager,
8268 getOrientation: () => this.orientation,
8269 openNew: () => {
8270 const fn = item.onOpenNew ?? item.onOpen;
8271 fn();
8272 },
8273 suppressTooltip: (on) => {
8274 if (on) {
8275 this.tooltip.classList.remove(
8276 "desktop-mode-dock__tooltip--visible"
8277 );
8278 }
8279 }
8280 });
8281 this.peekTeardowns.set(`system:${item.id}`, teardown);
8282 return applyFilters(
8283 HOOKS.DOCK_TILE_ELEMENT,
8284 tile2,
8285 ctx
8286 );
8287 }
8288 /**
8289 * Create a single dock icon tile.
8290 *
8291 * A tile is a vertical stack: the primary icon button, plus — for
8292 * multi-capable pages — an instance rail rendered below it showing one
8293 * dot per open window and a trailing "+" to open another. The rail is
8294 * hydrated by {@link updateActiveStates}; here we only place the empty
8295 * container so the DOM is stable.
8296 */
8297 createItemButton(item) {
8298 const ctx = {
8299 ...this.buildHookContextBase(),
8300 item,
8301 isSystem: false
8302 };
8303 const tile2 = document.createElement("div");
8304 const baseClasses = ["desktop-mode-dock__item"];
8305 if (item.multi) {
8306 baseClasses.push("desktop-mode-dock__item--multi");
8307 }
8308 const filteredClasses = applyFilters(
8309 HOOKS.DOCK_TILE_CLASS,
8310 baseClasses,
8311 ctx
8312 );
8313 tile2.className = filteredClasses.join(" ");
8314 tile2.dataset.menuSlug = item.id;
8315 const primary = document.createElement("button");
8316 primary.className = "desktop-mode-dock__item-primary";
8317 primary.setAttribute("type", "button");
8318 primary.setAttribute("aria-label", item.title);
8319 const iconEl = this.resolveIcon(item.icon, item.title, item.url);
8320 primary.appendChild(iconEl);
8321 if (item.badge > 0) {
8322 const displayCount = item.badge > 99 ? "99+" : String(item.badge);
8323 const badge = document.createElement("span");
8324 badge.className = "desktop-mode-dock__badge";
8325 badge.textContent = displayCount;
8326 badge.setAttribute(
8327 "aria-label",
8328 sprintf(
8329 // translators: %d is the number of pending updates / items.
8330 _n("%d update", "%d updates", item.badge),
8331 item.badge
8332 )
8333 );
8334 primary.appendChild(badge);
8335 }
8336 primary.addEventListener("click", () => {
8337 this.openPage(item);
8338 });
8339 tile2.addEventListener("contextmenu", (ev) => {
8340 ev.preventDefault();
8341 openItemVisibilityMenu({
8342 x: ev.clientX,
8343 y: ev.clientY,
8344 id: item.id,
8345 title: item.title,
8346 surface: "dock",
8347 pluginFile: item.pluginFile ?? null,
8348 pluginName: item.pluginName ?? null
8349 });
8350 });
8351 tile2.appendChild(primary);
8352 this.bindTooltipFiltered(tile2, item.title, ctx);
8353 const baseId = this.resolveItemBaseId(item);
8354 const teardown = attachDockPeek({
8355 tile: tile2,
8356 item: {
8357 id: item.id,
8358 title: item.title,
8359 icon: item.icon,
8360 url: item.url
8361 },
8362 // Source instances from `getAllByBaseId` regardless of
8363 // `item.multi`. The Ghost Card spawns duplicates on every
8364 // tile (the `enableGhost: true` below), so any tile —
8365 // including ones synthesized from a desktop icon, where
8366 // `multi` is never set — can end up with >1 open instance.
8367 // A `multi`-gated singleton lookup would only return the
8368 // first window and the peek would silently underreport.
8369 // For genuine singletons that never get duplicated, the
8370 // returned array is just `[one]` (or `[]`), same shape the
8371 // old branch produced.
8372 getInstances: () => this.windowManager.getAllByBaseIdOnActiveDesktop(baseId),
8373 // Ghost Card on EVERY tile, regardless of `multi`. The
8374 // affordance reads consistently across the dock — every
8375 // hover-peek surfaces a "+ open another <Page>" card. For
8376 // multi-capable items, clicking it spawns a fresh
8377 // instance. For singletons it falls through to the same
8378 // open-or-focus path the tile click takes — usually a
8379 // no-op (focuses the existing window) but cheap and
8380 // visually consistent.
8381 enableGhost: true,
8382 windowManager: this.windowManager,
8383 getOrientation: () => this.orientation,
8384 openNew: () => this.openNewInstance(item),
8385 suppressTooltip: (on) => {
8386 if (on) {
8387 this.tooltip.classList.remove(
8388 "desktop-mode-dock__tooltip--visible"
8389 );
8390 }
8391 }
8392 });
8393 this.peekTeardowns.set(item.id, teardown);
8394 this.attachDragReorder(tile2, item.id);
8395 return applyFilters(
8396 HOOKS.DOCK_TILE_ELEMENT,
8397 tile2,
8398 ctx
8399 );
8400 }
8401 /**
8402 * Drag-to-reorder for menu tiles. Fixed slots — no interpolated
8403 * positioning. While dragging:
8404 *
8405 * 1. Pointer down on the primary button starts a tentative drag.
8406 * Click handling is preserved by requiring movement past a
8407 * small threshold before we claim the gesture.
8408 * 2. Once claimed, the tile gets a `--dragging` modifier so CSS
8409 * can lift it visually. Every `pointermove` checks which other
8410 * menu tile the cursor is currently over; if it's a different
8411 * tile, we splice the dragged tile in front of it (so adjacent
8412 * tiles slide into the vacated slot).
8413 * 3. On `pointerup` we read the resulting DOM order, persist the
8414 * new id list to `dockOrder` via the public settings writer,
8415 * and the layout-dispatcher subscriber re-applies. Cancellation
8416 * (Escape, pointercancel) reverts to the original order.
8417 *
8418 * @since 0.8.2
8419 */
8420 attachDragReorder(tile2, itemId) {
8421 const THRESHOLD = 5;
8422 const FLIP_MS = 200;
8423 let active2 = false;
8424 let startX = 0;
8425 let startY = 0;
8426 let originalOrder = [];
8427 let originalNext = null;
8428 let pointerId = -1;
8429 let originRect = null;
8430 let justDragged = false;
8431 const hardReset = () => {
8432 active2 = false;
8433 tile2.classList.remove("desktop-mode-dock__item--dragging");
8434 tile2.style.transform = "";
8435 tile2.style.transition = "";
8436 document.removeEventListener("pointermove", onMove);
8437 document.removeEventListener("pointerup", onUp);
8438 document.removeEventListener("pointercancel", onCancel);
8439 document.removeEventListener("keydown", onKey, true);
8440 window.removeEventListener("blur", onBlur);
8441 document.removeEventListener("visibilitychange", onVisibility);
8442 pointerId = -1;
8443 originRect = null;
8444 };
8445 const isMenuTile = (el) => {
8446 return !!el && el instanceof HTMLElement && el.classList.contains("desktop-mode-dock__item") && !el.classList.contains("desktop-mode-dock__item--system") && !!el.dataset.menuSlug;
8447 };
8448 const eachSiblingTile = (fn) => {
8449 for (const child of Array.from(this.itemHost.children)) {
8450 if (child instanceof HTMLElement && child !== tile2 && isMenuTile(child)) {
8451 fn(child);
8452 }
8453 }
8454 };
8455 const snapshotMenuOrder = () => {
8456 const ids = [];
8457 for (const child of Array.from(this.itemHost.children)) {
8458 if (isMenuTile(child)) {
8459 ids.push(child.dataset.menuSlug);
8460 }
8461 }
8462 return ids;
8463 };
8464 const flipSiblings = (prevRects) => {
8465 eachSiblingTile((sib) => {
8466 const prev = prevRects.get(sib);
8467 if (!prev) {
8468 return;
8469 }
8470 const now = sib.getBoundingClientRect();
8471 const dx = prev.left - now.left;
8472 const dy = prev.top - now.top;
8473 if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) {
8474 return;
8475 }
8476 sib.style.transition = "none";
8477 sib.style.transform = `translate(${dx}px, ${dy}px)`;
8478 void sib.offsetHeight;
8479 sib.style.transition = `transform ${FLIP_MS}ms cubic-bezier(0.2, 0.7, 0.3, 1)`;
8480 sib.style.transform = "";
8481 const onEnd = () => {
8482 sib.style.transition = "";
8483 sib.style.transform = "";
8484 sib.removeEventListener("transitionend", onEnd);
8485 };
8486 sib.addEventListener("transitionend", onEnd);
8487 });
8488 };
8489 const onMove = (ev) => {
8490 if (pointerId !== -1 && ev.pointerId !== pointerId) {
8491 return;
8492 }
8493 if (!active2) {
8494 const dx2 = ev.clientX - startX;
8495 const dy2 = ev.clientY - startY;
8496 if (dx2 * dx2 + dy2 * dy2 < THRESHOLD * THRESHOLD) {
8497 return;
8498 }
8499 active2 = true;
8500 originalOrder = snapshotMenuOrder();
8501 originalNext = tile2.nextSibling;
8502 originRect = tile2.getBoundingClientRect();
8503 tile2.classList.add("desktop-mode-dock__item--dragging");
8504 this.tooltip.classList.remove(
8505 "desktop-mode-dock__tooltip--visible"
8506 );
8507 }
8508 if (!originRect) {
8509 return;
8510 }
8511 const dx = ev.clientX - startX;
8512 const dy = ev.clientY - startY;
8513 tile2.style.transform = `translate(${dx}px, ${dy}px)`;
8514 const under = document.elementFromPoint(ev.clientX, ev.clientY);
8515 const targetTile = under?.closest(
8516 ".desktop-mode-dock__item"
8517 );
8518 if (!targetTile || targetTile === tile2) {
8519 return;
8520 }
8521 if (!isMenuTile(targetTile)) {
8522 return;
8523 }
8524 const rect = targetTile.getBoundingClientRect();
8525 let insertBefore;
8526 if (this.orientation === "bottom") {
8527 insertBefore = ev.clientX < rect.left + rect.width / 2;
8528 } else {
8529 insertBefore = ev.clientY < rect.top + rect.height / 2;
8530 }
8531 const prevRects = /* @__PURE__ */ new Map();
8532 eachSiblingTile((sib) => {
8533 prevRects.set(sib, sib.getBoundingClientRect());
8534 });
8535 let reordered = false;
8536 if (insertBefore) {
8537 if (targetTile !== tile2.nextSibling) {
8538 this.itemHost.insertBefore(tile2, targetTile);
8539 reordered = true;
8540 }
8541 } else if (targetTile.nextSibling !== tile2) {
8542 this.itemHost.insertBefore(tile2, targetTile.nextSibling);
8543 reordered = true;
8544 }
8545 if (reordered) {
8546 tile2.style.transform = "";
8547 const fresh = tile2.getBoundingClientRect();
8548 startX = fresh.left + fresh.width / 2;
8549 startY = fresh.top + fresh.height / 2;
8550 tile2.style.transform = `translate(${ev.clientX - startX}px, ${ev.clientY - startY}px)`;
8551 flipSiblings(prevRects);
8552 }
8553 };
8554 const cleanup = () => {
8555 tile2.classList.remove("desktop-mode-dock__item--dragging");
8556 tile2.style.transform = "";
8557 tile2.style.transition = "";
8558 document.removeEventListener("pointermove", onMove);
8559 document.removeEventListener("pointerup", onUp);
8560 document.removeEventListener("pointercancel", onCancel);
8561 document.removeEventListener("keydown", onKey, true);
8562 window.removeEventListener("blur", onBlur);
8563 document.removeEventListener("visibilitychange", onVisibility);
8564 pointerId = -1;
8565 originRect = null;
8566 active2 = false;
8567 if (_Dock.activeDragReset === hardReset) {
8568 _Dock.activeDragReset = null;
8569 }
8570 };
8571 const animateHome = () => {
8572 tile2.style.transition = `transform ${FLIP_MS}ms cubic-bezier(0.2, 0.7, 0.3, 1)`;
8573 tile2.style.transform = "";
8574 const onEnd = () => {
8575 tile2.style.transition = "";
8576 tile2.removeEventListener("transitionend", onEnd);
8577 };
8578 tile2.addEventListener("transitionend", onEnd);
8579 };
8580 const persistDockOrder = (finalOrder) => {
8581 const api = window.wp?.desktop;
8582 if (!api?.getOsSettings || !api?.updateOsSettings) {
8583 return;
8584 }
8585 const existing = api.getOsSettings().dockOrder;
8586 const finalSet = new Set(finalOrder);
8587 const merged = [];
8588 let injected = false;
8589 for (const id of existing) {
8590 if (finalSet.has(id)) {
8591 if (!injected) {
8592 merged.push(...finalOrder);
8593 injected = true;
8594 }
8595 continue;
8596 }
8597 merged.push(id);
8598 }
8599 if (!injected) {
8600 merged.push(...finalOrder);
8601 }
8602 api.updateOsSettings({ dockOrder: merged });
8603 };
8604 const onUp = (ev) => {
8605 if (pointerId !== -1 && ev.pointerId !== pointerId) {
8606 return;
8607 }
8608 if (!active2) {
8609 cleanup();
8610 return;
8611 }
8612 justDragged = true;
8613 const finalOrder = snapshotMenuOrder();
8614 animateHome();
8615 cleanup();
8616 const same = finalOrder.length === originalOrder.length && finalOrder.every((id, i) => id === originalOrder[i]);
8617 if (!same) {
8618 persistDockOrder(finalOrder);
8619 }
8620 setTimeout(() => {
8621 justDragged = false;
8622 }, 200);
8623 };
8624 const onCancel = (ev) => {
8625 if (ev && pointerId !== -1 && ev.pointerId !== pointerId) {
8626 return;
8627 }
8628 if (active2 && originalNext !== void 0) {
8629 const prevRects = /* @__PURE__ */ new Map();
8630 eachSiblingTile((sib) => {
8631 prevRects.set(sib, sib.getBoundingClientRect());
8632 });
8633 this.itemHost.insertBefore(tile2, originalNext);
8634 flipSiblings(prevRects);
8635 }
8636 animateHome();
8637 cleanup();
8638 };
8639 const onKey = (ev) => {
8640 if (ev.key === "Escape") {
8641 onCancel();
8642 }
8643 };
8644 const onBlur = () => onCancel();
8645 const onVisibility = () => {
8646 if (document.visibilityState !== "visible") {
8647 onCancel();
8648 }
8649 };
8650 tile2.addEventListener("pointerdown", (ev) => {
8651 if (ev.button !== 0) {
8652 return;
8653 }
8654 if (_Dock.activeDragReset) {
8655 const prev = _Dock.activeDragReset;
8656 _Dock.activeDragReset = null;
8657 prev();
8658 }
8659 if (active2 || pointerId !== -1) {
8660 hardReset();
8661 }
8662 startX = ev.clientX;
8663 startY = ev.clientY;
8664 pointerId = ev.pointerId;
8665 active2 = false;
8666 _Dock.activeDragReset = hardReset;
8667 document.addEventListener("pointermove", onMove);
8668 document.addEventListener("pointerup", onUp);
8669 document.addEventListener("pointercancel", onCancel);
8670 document.addEventListener("keydown", onKey, true);
8671 window.addEventListener("blur", onBlur);
8672 document.addEventListener("visibilitychange", onVisibility);
8673 });
8674 tile2.addEventListener(
8675 "click",
8676 (ev) => {
8677 if (justDragged) {
8678 ev.preventDefault();
8679 ev.stopImmediatePropagation();
8680 }
8681 },
8682 true
8683 );
8684 }
8685 /**
8686 * Resolve a registered icon value into a DOM element.
8687 *
8688 * Priority: dashicons class → inline SVG data URI → image URL →
8689 * letter badge derived from the item's title. The letter fallback is
8690 * important for plugin tiles: plugin authors routinely register
8691 * top-level menus with `add_menu_page()` and omit the icon argument
8692 * (defaulting to `'div'` or empty), which would otherwise render as
8693 * an indistinguishable wall of generic wrenches. A colored letter
8694 * tile gives each plugin a stable, unique-ish visual identity with
8695 * zero plugin-side effort — the hue derives deterministically from
8696 * the title so the same plugin always gets the same color.
8697 *
8698 * @param icon The icon value from the menu entry.
8699 * @param title Human-readable title, used when falling back to a
8700 * letter badge.
8701 */
8702 resolveIcon(icon, title, url) {
8703 if (icon.startsWith("dashicons-") && icon !== "dashicons-admin-generic") {
8704 const el = document.createElement("span");
8705 el.className = `dashicons ${icon}`;
8706 el.setAttribute("aria-hidden", "true");
8707 return el;
8708 }
8709 if (icon.startsWith("data:image/svg+xml;base64,")) {
8710 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
8711 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
8712 return this._makeSvgIcon(icon);
8713 }
8714 }
8715 if (icon.startsWith("url(")) {
8716 return this._makeSvgIcon(icon);
8717 }
8718 if (icon.startsWith("http://") || icon.startsWith("https://")) {
8719 const img = document.createElement("img");
8720 img.className = "desktop-mode-dock__item-img";
8721 img.src = icon;
8722 img.alt = "";
8723 img.setAttribute("aria-hidden", "true");
8724 return img;
8725 }
8726 if (url) {
8727 const native = this._extractNativeMenuIcon(url);
8728 if (native) {
8729 return native;
8730 }
8731 }
8732 if (icon === "dashicons-admin-generic") {
8733 const el = document.createElement("span");
8734 el.className = "dashicons dashicons-admin-generic";
8735 el.setAttribute("aria-hidden", "true");
8736 return el;
8737 }
8738 return this.createLetterBadge(title);
8739 }
8740 /**
8741 * Build an SVG-background icon tile. Shared between the data-URI
8742 * branch of {@link resolveIcon} and the native-menu extractor.
8743 */
8744 _makeSvgIcon(bgValue) {
8745 const el = document.createElement("span");
8746 el.className = "desktop-mode-dock__item-svg";
8747 el.style.backgroundImage = bgValue.startsWith("url(") ? bgValue : `url("${bgValue}")`;
8748 el.style.backgroundSize = "contain";
8749 el.style.backgroundRepeat = "no-repeat";
8750 el.style.backgroundPosition = "center";
8751 el.setAttribute("aria-hidden", "true");
8752 return el;
8753 }
8754 /**
8755 * Extract a plugin's icon from the hidden `#adminmenu` that still
8756 * exists in the parent shell DOM (display:none'd by desktop.css).
8757 * Handles the three shapes plugins commonly use when the menu-page
8758 * icon_url is 'none' or 'div':
8759 *
8760 * (a) `<img src="...">` nested inside `.wp-menu-image`
8761 * (b) a dashicon class on `.wp-menu-image` itself
8762 * (c) a CSS background-image on `.wp-menu-image::before` (the
8763 * `menu-icon-XYZ` pattern Yoast, WooCommerce, Jetpack, etc. use)
8764 *
8765 * Returns null when the URL doesn't match any admin-menu entry or
8766 * none of the three shapes are detectable.
8767 */
8768 _extractNativeMenuIcon(url) {
8769 const adminMenu = document.getElementById("adminmenu");
8770 if (!adminMenu) {
8771 return null;
8772 }
8773 let target2;
8774 try {
8775 const u = new URL(url, window.location.href);
8776 const filename = u.pathname.split("/").pop() || "";
8777 target2 = filename + u.search;
8778 } catch {
8779 return null;
8780 }
8781 if (!target2) {
8782 return null;
8783 }
8784 const links = adminMenu.querySelectorAll("li.menu-top > a");
8785 let matchLi = null;
8786 for (const link of Array.from(links)) {
8787 if (link.href.endsWith(target2)) {
8788 matchLi = link.closest("li.menu-top");
8789 break;
8790 }
8791 }
8792 if (!matchLi) {
8793 return null;
8794 }
8795 const imgWrap = matchLi.querySelector(".wp-menu-image");
8796 if (!imgWrap) {
8797 return null;
8798 }
8799 const img = imgWrap.querySelector("img");
8800 if (img && img.src) {
8801 const el = document.createElement("img");
8802 el.className = "desktop-mode-dock__item-img";
8803 el.src = img.src;
8804 el.alt = "";
8805 el.setAttribute("aria-hidden", "true");
8806 return el;
8807 }
8808 const dashMatch = imgWrap.className.match(/\bdashicons-[\w-]+\b/);
8809 if (dashMatch && dashMatch[0] !== "dashicons-before") {
8810 const el = document.createElement("span");
8811 el.className = `dashicons ${dashMatch[0]}`;
8812 el.setAttribute("aria-hidden", "true");
8813 return el;
8814 }
8815 const before = window.getComputedStyle(imgWrap, "::before");
8816 const bg = before.backgroundImage;
8817 if (bg && bg !== "none" && !bg.includes('url("")')) {
8818 return this._makeSvgIcon(bg);
8819 }
8820 const bgWrap = window.getComputedStyle(imgWrap).backgroundImage;
8821 if (bgWrap && bgWrap !== "none" && !bgWrap.includes('url("")')) {
8822 return this._makeSvgIcon(bgWrap);
8823 }
8824 return null;
8825 }
8826 /**
8827 * Create a letter-badge icon — a rounded square tinted with a
8828 * deterministic hue derived from the title, displaying the first
8829 * letter of the title. Mirrors the "app icon placeholder" look
8830 * macOS uses when an app ships without artwork.
8831 *
8832 * The title always drives both the letter and the hue — same plugin,
8833 * same color across reloads. An empty title falls through to a `?`
8834 * on a neutral gray tile, but the menu builder upstream guards
8835 * against empty titles, so this is a defensive branch.
8836 */
8837 createLetterBadge(title) {
8838 const el = document.createElement("span");
8839 el.className = "desktop-mode-dock__item-letter";
8840 el.setAttribute("aria-hidden", "true");
8841 const trimmed = title.trim();
8842 const firstCodePoint = trimmed ? Array.from(trimmed)[0] : "?";
8843 el.textContent = firstCodePoint.toUpperCase();
8844 const hue = hashTitleToHue(trimmed);
8845 el.style.background = `linear-gradient(135deg, hsl(${hue} 62% 55%), hsl(${(hue + 24) % 360} 58% 42%))`;
8846 return el;
8847 }
8848 /**
8849 * Bind tooltip show/hide on hover. Tooltip anchor differs per
8850 * orientation: left dock → tile's right side, right dock → tile's
8851 * left side, bottom dock → above the tile. We set the relevant
8852 * coordinate inline each enter; the CSS takes care of the rest.
8853 */
8854 /**
8855 * Resolves the tooltip text through {@link HOOKS.DOCK_TILE_TOOLTIP}
8856 * once at bind time (so the dock doesn't re-filter on every
8857 * pointerenter) and stashes the resolved text on
8858 * `tile.dataset.dockTooltip` so the multi-instance chip can
8859 * restore it on its own pointerleave without going through the
8860 * filter again.
8861 *
8862 * Returning an empty string from the filter suppresses the
8863 * tooltip — the listener short-circuits and never adds the
8864 * `--visible` class.
8865 */
8866 bindTooltipFiltered(tile2, text, ctx) {
8867 const filtered = applyFilters(
8868 HOOKS.DOCK_TILE_TOOLTIP,
8869 text,
8870 ctx
8871 );
8872 tile2.dataset.dockTooltip = filtered;
8873 if (filtered === "") {
8874 return;
8875 }
8876 tile2.addEventListener("pointerenter", () => {
8877 this.positionTooltip(tile2, filtered);
8878 this.tooltip.classList.add("desktop-mode-dock__tooltip--visible");
8879 });
8880 tile2.addEventListener("pointerleave", () => {
8881 this.tooltip.classList.remove("desktop-mode-dock__tooltip--visible");
8882 });
8883 }
8884 /**
8885 * Write the tooltip text + anchor coordinate for `el`. Split out
8886 * because the multi-instance chip's pointerenter handler also
8887 * needs to anchor to a specific element (the chip, not the tile).
8888 */
8889 positionTooltip(el, text) {
8890 const rect = el.getBoundingClientRect();
8891 this.tooltip.textContent = text;
8892 if (this.orientation === "bottom") {
8893 this.tooltip.style.left = `${rect.left + rect.width / 2}px`;
8894 this.tooltip.style.top = `${rect.top - 14}px`;
8895 } else if (this.orientation === "right") {
8896 this.tooltip.style.top = `${rect.top + rect.height / 2 - 14}px`;
8897 this.tooltip.style.left = `${rect.left}px`;
8898 } else {
8899 this.tooltip.style.top = `${rect.top + rect.height / 2 - 14}px`;
8900 this.tooltip.style.left = `${rect.right + 8}px`;
8901 }
8902 }
8903 /**
8904 * Open an admin page in a window (or focus if already open).
8905 *
8906 * Consults the native URL-remap registry first — when an opt-in
8907 * native window has registered itself as the replacement for this
8908 * admin URL (e.g. the native Posts window for `edit.php` when the
8909 * user has flipped `nativePostsEnabled`), the click is rerouted
8910 * to that window and the iframe path is skipped. The dock item
8911 * itself is untouched: same icon, same tooltip, same position —
8912 * only the destination changes.
8913 */
8914 openPage(item) {
8915 if (item.id.startsWith("dock:")) {
8916 const iconId = item.id.slice(5);
8917 const cfg = window.desktopModeConfig;
8918 const icon = cfg?.desktopIcons?.find((i) => i.id === iconId);
8919 if (icon?.window) {
8920 const wp = window.wp?.desktop;
8921 wp?.openWindow?.(icon.window);
8922 return;
8923 }
8924 if (icon?.url) {
8925 if (tryOpenExternalUrl(icon.url)) {
8926 return;
8927 }
8928 const baseId2 = this.deriveWindowId(icon.url);
8929 this.windowManager.open({
8930 id: baseId2,
8931 baseId: baseId2,
8932 url: icon.url,
8933 parentUrl: icon.url,
8934 title: icon.title,
8935 icon: icon.icon.startsWith("dashicons-") ? icon.icon : "dashicons-admin-generic",
8936 submenu: [],
8937 multi: false
8938 });
8939 return;
8940 }
8941 return;
8942 }
8943 if (tryOpenExternalUrl(item.url)) {
8944 return;
8945 }
8946 if (tryNativeUrlRemap(item.url)) {
8947 return;
8948 }
8949 const baseId = this.deriveWindowId(item.url);
8950 this.windowManager.open({
8951 id: baseId,
8952 baseId,
8953 url: item.url,
8954 parentUrl: item.url,
8955 title: item.title,
8956 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
8957 submenu: item.submenu,
8958 multi: !!item.multi
8959 });
8960 }
8961 /**
8962 * Open a brand-new instance of a page, even if one is already
8963 * open. Invoked by the "+" ghost card in the dock peek.
8964 *
8965 * The user explicitly asked for "another window of this thing,"
8966 * so we honour the request even when {@link tryNativeUrlRemap}
8967 * would otherwise route the click into a native-window
8968 * singleton. Result: clicking + while a native Posts window is
8969 * open opens a fresh iframe of `edit.php` alongside it. Two
8970 * windows of Posts is the explicit ask — that's what + is for.
8971 */
8972 openNewInstance(item) {
8973 if (tryOpenExternalUrl(item.url)) {
8974 return;
8975 }
8976 const openNewWindow = window.wp?.desktop?.openNewWindow;
8977 if (item.windowId && !item.url) {
8978 if (openNewWindow?.(item.windowId, { source: "dock-peek" })) {
8979 return;
8980 }
8981 }
8982 const remappedId = resolveNativeUrlRemap(item.url);
8983 if (remappedId) {
8984 if (openNewWindow?.(remappedId, { source: "dock-peek" })) {
8985 return;
8986 }
8987 }
8988 const baseId = this.deriveWindowId(item.url);
8989 void this.windowManager.openNew({
8990 id: baseId,
8991 baseId,
8992 url: item.url,
8993 parentUrl: item.url,
8994 title: item.title,
8995 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
8996 submenu: item.submenu,
8997 multi: true
8998 });
8999 }
9000 /**
9001 * Derive a window ID from an admin page URL.
9002 */
9003 deriveWindowId(url) {
9004 return deriveWindowId(url, this.adminUrl);
9005 }
9006 /**
9007 * Resolve the window-manager key for a dock tile, in this order:
9008 *
9009 * 1. `item.windowId` — set by `applyDockPlacement` when the tile
9010 * is synthesized from a `desktop_mode_register_icon()` entry
9011 * whose target is a native window. Native-window ids never
9012 * pass through the URL → native-window remap layer, so we
9013 * short-circuit before touching it.
9014 * 2. {@link resolveNativeUrlRemap} on `item.url` — captures the
9015 * `nativePostsEnabled` / `nativePagesEnabled` opt-ins that
9016 * repoint a URL-based tile at a native window.
9017 * 3. {@link deriveWindowId} on `item.url` — the URL-based
9018 * fallback for ordinary admin-menu tiles.
9019 *
9020 * Shared by the hover-peek card and the active/focused-dot
9021 * indicator; the two stayed in lockstep before this method existed
9022 * by hand-rolling the same chain at each call site.
9023 */
9024 resolveItemBaseId(item) {
9025 if (item.windowId) {
9026 return item.windowId;
9027 }
9028 if (item.id.startsWith("dock:")) {
9029 const iconId = item.id.slice(5);
9030 const cfg = window.desktopModeConfig;
9031 const icon = cfg?.desktopIcons?.find((i) => i.id === iconId);
9032 if (icon?.window) {
9033 return icon.window;
9034 }
9035 if (icon?.url) {
9036 const remapped2 = resolveNativeUrlRemap(icon.url);
9037 return remapped2 ?? this.deriveWindowId(icon.url);
9038 }
9039 }
9040 const remapped = resolveNativeUrlRemap(item.url);
9041 return remapped ?? this.deriveWindowId(item.url);
9042 }
9043 /**
9044 * Listen to window events to update active/focused/minimized
9045 * indicators on dock items, plus the global Show Desktop body class.
9046 *
9047 * The event detail isn't used — we just need to re-query the
9048 * window manager on every change — so the handlers take no
9049 * argument and the type cast is gone with it.
9050 *
9051 * `WINDOW_MINIMIZED` / `WINDOW_RESTORED` route through the hook bus
9052 * (no DOM CustomEvent equivalent today). Without these, minimizing
9053 * a window via Show Desktop / the title-bar minimize button left
9054 * the dock's active-dot rendering stuck on "visible window" — the
9055 * user had no cue that everything had collapsed to minimized.
9056 */
9057 bindWindowEvents() {
9058 const refresh = () => this.updateActiveStates();
9059 this.boundRefresh = refresh;
9060 document.addEventListener("desktop-mode-window-opened", refresh);
9061 document.addEventListener("desktop-mode-window-closed", refresh);
9062 document.addEventListener("desktop-mode-window-focused", refresh);
9063 window.wp?.hooks?.addAction?.(
9064 "desktop-mode.desktop.switched",
9065 this.hooksNamespace,
9066 refresh
9067 );
9068 window.wp?.hooks?.addAction?.(
9069 "desktop-mode.desktop.closed",
9070 this.hooksNamespace,
9071 refresh
9072 );
9073 window.wp?.hooks?.addAction?.(
9074 HOOKS.WINDOW_MINIMIZED,
9075 this.hooksNamespace,
9076 refresh
9077 );
9078 window.wp?.hooks?.addAction?.(
9079 HOOKS.WINDOW_RESTORED,
9080 this.hooksNamespace,
9081 refresh
9082 );
9083 }
9084 /**
9085 * Tear the dock down: detach window-lifecycle listeners, clear
9086 * pending attention timers, remove the floating tooltip from
9087 * `document.body`, and empty the container's children. Used by
9088 * the layout dispatcher when the user switches `desktopLayout`
9089 * in OS Settings — old dock(s) get destroyed and a fresh set is
9090 * constructed for the new layout.
9091 *
9092 * Idempotent: calling twice is safe.
9093 */
9094 destroy() {
9095 document.removeEventListener(
9096 "desktop-mode-window-opened",
9097 this.boundRefresh
9098 );
9099 document.removeEventListener(
9100 "desktop-mode-window-closed",
9101 this.boundRefresh
9102 );
9103 document.removeEventListener(
9104 "desktop-mode-window-focused",
9105 this.boundRefresh
9106 );
9107 window.wp?.hooks?.removeAction?.(
9108 "desktop-mode.desktop.switched",
9109 this.hooksNamespace
9110 );
9111 window.wp?.hooks?.removeAction?.(
9112 "desktop-mode.desktop.closed",
9113 this.hooksNamespace
9114 );
9115 window.wp?.hooks?.removeAction?.(
9116 HOOKS.WINDOW_MINIMIZED,
9117 this.hooksNamespace
9118 );
9119 window.wp?.hooks?.removeAction?.(
9120 HOOKS.WINDOW_RESTORED,
9121 this.hooksNamespace
9122 );
9123 for (const handle of this.attentionTimers.values()) {
9124 window.clearTimeout(handle);
9125 }
9126 this.attentionTimers.clear();
9127 for (const teardown of this.peekTeardowns.values()) {
9128 teardown();
9129 }
9130 this.peekTeardowns.clear();
9131 this.tooltip.remove();
9132 while (this.container.firstChild) {
9133 this.container.removeChild(this.container.firstChild);
9134 }
9135 this.itemElements.clear();
9136 this.systemItemElements.clear();
9137 this.systemItems = [];
9138 this.systemSeparator = null;
9139 this.container.removeAttribute("data-desktop-mode-dock-placement");
9140 }
9141 /**
9142 * Update the active/focused/minimized classes on every dock item in
9143 * response to a window lifecycle event, and toggle the global Show
9144 * Desktop body class.
9145 *
9146 * For singletons the rail is absent; "active" means "the one window
9147 * is open". For multi-capable items, active means "≥1 instance is
9148 * open" and focused means "the focused window belongs to this item".
9149 *
9150 * `--all-minimized` is layered on top of `--active` and fires only
9151 * when EVERY open instance of the tile is minimized — so a partial
9152 * minimize (one of two windows hidden) keeps the solid dot. CSS
9153 * swaps the dot for a hollow ring on minimized-only tiles so the
9154 * user can tell at a glance "I have something here, it's just
9155 * hidden right now."
9156 */
9157 updateActiveStates() {
9158 const focused = this.windowManager.getFocused();
9159 const activeDesktopId = this.windowManager.getActiveDesktopId();
9160 const onActiveDesktop = (w) => (w.config.desktopId || activeDesktopId) === activeDesktopId;
9161 const isMinimized = (w) => w.state === "minimized";
9162 for (const item of this.items) {
9163 const tile2 = this.itemElements.get(item.id);
9164 if (!tile2) {
9165 continue;
9166 }
9167 const baseId = this.resolveItemBaseId(item);
9168 let instances = this.windowManager.getAllByBaseId(baseId).filter(onActiveDesktop);
9169 if (instances.length === 0 && item.url) {
9170 const derivedId = this.deriveWindowId(item.url);
9171 instances = this.windowManager.getAll().filter((w) => {
9172 const wBase = w.config.baseId || w.id;
9173 if (wBase === baseId || wBase === derivedId || wBase === item.id) {
9174 return true;
9175 }
9176 if (w.config.url) {
9177 const wDerived = this.deriveWindowId(w.config.url);
9178 return wDerived === baseId || wDerived === derivedId;
9179 }
9180 return false;
9181 }).filter(onActiveDesktop);
9182 }
9183 const isOpen = instances.length > 0;
9184 const allMinimized = isOpen && instances.every(isMinimized);
9185 const isFocused = !!focused && onActiveDesktop(focused) && !isMinimized(focused) && instances.some((w) => w.id === focused.id || (focused.config.baseId || focused.id) === baseId);
9186 tile2.classList.toggle("desktop-mode-dock__item--active", isOpen);
9187 tile2.classList.toggle("desktop-mode-dock__item--focused", isFocused);
9188 tile2.classList.toggle(
9189 "desktop-mode-dock__item--all-minimized",
9190 allMinimized
9191 );
9192 tile2.classList.toggle(
9193 "desktop-mode-dock__item--stacked",
9194 isOpen && instances.length > 1
9195 );
9196 }
9197 for (const sys of this.systemItems) {
9198 const tile2 = this.systemItemElements.get(sys.id);
9199 if (!tile2) {
9200 continue;
9201 }
9202 const sysWin = this.windowManager.getById(sys.id);
9203 const isOpen = sys.isOpen ? sys.isOpen() : !!sysWin;
9204 const allMinimized = !!sysWin && isMinimized(sysWin);
9205 const isFocused = !!focused && focused.id === sys.id && !isMinimized(focused);
9206 tile2.classList.toggle("desktop-mode-dock__item--active", isOpen);
9207 tile2.classList.toggle("desktop-mode-dock__item--focused", isFocused);
9208 tile2.classList.toggle(
9209 "desktop-mode-dock__item--all-minimized",
9210 allMinimized
9211 );
9212 }
9213 this.updateShowDesktopBodyClass();
9214 }
9215 /**
9216 * Toggle `body.desktop-mode-show-desktop-active` based on whether
9217 * every live window on the active desktop is minimized. Mirrors
9218 * the heuristic inside {@link WindowManager.toggleShowDesktop} so
9219 * the visual cue tracks the actual state — set by Show Desktop
9220 * gestures, restored when any window is brought back, automatically
9221 * cleared when no windows exist.
9222 *
9223 * @internal
9224 */
9225 updateShowDesktopBodyClass() {
9226 const activeDesktopId = this.windowManager.getActiveDesktopId();
9227 const live = this.windowManager.getAll().filter(
9228 (w) => (w.config.desktopId || activeDesktopId) === activeDesktopId
9229 );
9230 const showDesktop = live.length > 0 && live.every((w) => w.state === "minimized");
9231 document.body.classList.toggle(
9232 "desktop-mode-show-desktop-active",
9233 showDesktop
9234 );
9235 }
9236 };
9237 _Dock.instanceCounter = 0;
9238 _Dock.activeDragReset = null;
9239 let Dock = _Dock;
9240 function _applyBadgeNode(host, count) {
9241 const existing = host.querySelector(
9242 ":scope > .desktop-mode-dock__badge"
9243 );
9244 if (count <= 0) {
9245 existing?.remove();
9246 return;
9247 }
9248 const display = count > 99 ? "99+" : String(count);
9249 if (existing) {
9250 if (existing.textContent !== display) {
9251 existing.textContent = display;
9252 }
9253 existing.setAttribute(
9254 "aria-label",
9255 sprintf(
9256 // translators: %d is the number of pending items in a dock badge.
9257 _n("%d notification", "%d notifications", count),
9258 count
9259 )
9260 );
9261 return;
9262 }
9263 const badge = document.createElement("span");
9264 badge.className = "desktop-mode-dock__badge";
9265 badge.textContent = display;
9266 badge.setAttribute(
9267 "aria-label",
9268 sprintf(
9269 // translators: %d is the number of pending items in a dock badge.
9270 _n("%d notification", "%d notifications", count),
9271 count
9272 )
9273 );
9274 host.appendChild(badge);
9275 }
9276 const DEFAULT_RENDERER_DOCK = Symbol.for(
9277 "desktop-mode/default-dock-rail-renderer/dock"
9278 );
9279 const defaultDockRailRenderer = {
9280 id: "default",
9281 label: "Icon strip",
9282 description: "The shipped baseline — icon tiles with badges, tooltips, multi-instance chips, and attention animations.",
9283 icon: "dashicons-menu-alt",
9284 apiVersion: 1,
9285 mount(deps2) {
9286 const dock = new Dock(
9287 deps2.container,
9288 deps2.windowManager,
9289 deps2.items,
9290 deps2.adminUrl,
9291 deps2.orientation
9292 );
9293 const controller = {
9294 [DEFAULT_RENDERER_DOCK]: dock,
9295 replaceItems: (items) => dock.replaceItems(items),
9296 appendSystemItem: (item) => dock.appendSystemItem(item),
9297 removeSystemItem: (id) => dock.removeSystemItem(id),
9298 setBadge: (itemId, count) => dock.setBadge(itemId, count),
9299 setAttention: (itemId, mode, opts) => dock.setAttention(itemId, mode, opts),
9300 setOrientation: (orientation) => dock.setOrientation(orientation),
9301 destroy: () => dock.destroy()
9302 };
9303 return controller;
9304 }
9305 };
9306 function unwrapDefaultDock(controller) {
9307 if (!controller) {
9308 return null;
9309 }
9310 const probe = controller;
9311 const dock = probe[DEFAULT_RENDERER_DOCK];
9312 return dock instanceof Dock ? dock : null;
9313 }
9314 function installDefaultDockRailRenderer() {
9315 register$2(defaultDockRailRenderer);
9316 }
9317 const modalStyles = css`:host{display:none;position:fixed;inset:0;align-items:center;justify-content:center;background:rgba( 0,0,0,0.45 );backdrop-filter:blur( 2px );z-index:10000;--desktop-mode-text:#f0f0f1;--desktop-mode-text-muted:#bbc1c7;--desktop-mode-muted:#a7aaad;--desktop-mode-muted-fg:#a7aaad;--desktop-mode-border:rgba( 255,255,255,0.25 );--desktop-mode-window-bg:#2c3338;--wpd-button-bg-hover:rgba( 255,255,255,0.08 )}:host( [ open ] ){display:flex}.dialog{max-width:92vw;max-height:90vh;background:var( --wpd-modal-bg,var( --desktop-mode-bg,#1d2327 ) );color:var( --wpd-modal-fg,var( --desktop-mode-fg,#fff ) );border:1px solid rgba( 255,255,255,0.08 );border-radius:10px;box-shadow:0 20px 50px rgba( 0,0,0,0.6 );display:flex;flex-direction:column;overflow:hidden}:host( [ size='sm' ] ) .dialog{width:min( 360px,92vw )}:host(:not( [ size ] ) ) .dialog,:host( [ size='md' ] ) .dialog{width:min( 540px,92vw )}:host( [ size='lg' ] ) .dialog{width:min( 760px,94vw )}.header{display:flex;align-items:center;gap:10px;padding:16px 20px 12px;border-bottom:1px solid rgba( 255,255,255,0.06 )}.title{margin:0;flex:1;font-size:15px;font-weight:600}.header-actions{display:flex;gap:6px}.header-actions::slotted( * ){margin-inline-start:6px}.close{background:transparent;border:0;color:inherit;font-size:18px;line-height:1;padding:4px 8px;border-radius:4px;cursor:pointer;opacity:0.7}.close:hover{opacity:1;background:rgba( 255,255,255,0.08 )}.body{padding:16px 20px;overflow:auto;flex:1 1 auto;font-size:13px;line-height:1.5}.footer{padding:12px 20px 16px;border-top:1px solid rgba( 255,255,255,0.06 )}.footer slot{display:flex;justify-content:flex-end;gap:10px;flex-wrap:wrap}:host( [ mandatory ] ) .close{display:none}`;
9318 const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
9319 const _WpdModal = class _WpdModal extends Component {
9320 constructor() {
9321 super(...arguments);
9322 this._prevFocus = null;
9323 this._onKey = (e) => {
9324 if (e.key === "Escape" && !this.hasAttribute("mandatory")) {
9325 e.preventDefault();
9326 this._cancel();
9327 return;
9328 }
9329 if (e.key === "Tab") {
9330 const f = this._focusables();
9331 if (f.length === 0) {
9332 return;
9333 }
9334 const first = f[0];
9335 const last = f[f.length - 1];
9336 const doc = this.ownerDocument;
9337 const fallback = doc ? doc.activeElement : null;
9338 const active2 = e.composedPath()[0] || fallback;
9339 if (e.shiftKey && active2 === first) {
9340 e.preventDefault();
9341 last.focus();
9342 } else if (!e.shiftKey && active2 === last) {
9343 e.preventDefault();
9344 first.focus();
9345 }
9346 }
9347 };
9348 this._onBackdrop = (e) => {
9349 if (this.hasAttribute("mandatory")) {
9350 return;
9351 }
9352 const path = e.composedPath();
9353 const original = path.length > 0 ? path[0] : e.target;
9354 if (original === this) {
9355 this._cancel();
9356 }
9357 };
9358 }
9359 connectedCallback() {
9360 super.connectedCallback();
9361 this.setAttribute("role", "dialog");
9362 this.setAttribute("aria-modal", "true");
9363 this.addEventListener("keydown", this._onKey);
9364 this.addEventListener("click", this._onBackdrop);
9365 }
9366 disconnectedCallback() {
9367 this.removeEventListener("keydown", this._onKey);
9368 this.removeEventListener("click", this._onBackdrop);
9369 }
9370 attributeChangedCallback(name, oldValue, newValue) {
9371 super.attributeChangedCallback?.(name, oldValue, newValue);
9372 if (name === "open") {
9373 if (newValue !== null) {
9374 const doc = this.ownerDocument;
9375 this._prevFocus = doc ? doc.activeElement : null;
9376 queueMicrotask(() => this._focusFirst());
9377 } else if (this._prevFocus) {
9378 try {
9379 this._prevFocus.focus();
9380 } catch (e) {
9381 }
9382 this._prevFocus = null;
9383 }
9384 }
9385 }
9386 showModal() {
9387 this.setAttribute("open", "");
9388 }
9389 hideModal() {
9390 this.removeAttribute("open");
9391 }
9392 _focusables() {
9393 const root = this.shadowRoot;
9394 if (!root) {
9395 return [];
9396 }
9397 const slotted = Array.from(this.querySelectorAll(FOCUSABLE));
9398 const inShadow = Array.from(root.querySelectorAll(FOCUSABLE));
9399 return [...slotted, ...inShadow].filter((el) => el.offsetParent !== null || el.tagName === "BUTTON");
9400 }
9401 _focusFirst() {
9402 const f = this._focusables();
9403 if (f.length > 0) {
9404 f[0].focus();
9405 } else {
9406 const inner = this.shadowRoot?.querySelector(".dialog");
9407 inner?.focus?.();
9408 }
9409 }
9410 _cancel() {
9411 const ev = new CustomEvent("wpd-modal-cancel", {
9412 bubbles: true,
9413 cancelable: true,
9414 composed: true
9415 });
9416 const allowed = this.dispatchEvent(ev);
9417 if (allowed) {
9418 this.hideModal();
9419 }
9420 }
9421 render() {
9422 const title = this.getAttribute("title") ?? "";
9423 const mandatory = this.hasAttribute("mandatory");
9424 return html`
9425 <div class="dialog" tabindex="-1">
9426 ${title ? html`
9427 <div class="header">
9428 <h2 class="title">${title}</h2>
9429 <div class="header-actions">
9430 <slot name="header-actions"></slot>
9431 ${mandatory ? html`` : html`<button
9432 type="button"
9433 class="close"
9434 aria-label="Close"
9435 @click=${() => this._cancel()}
9436 >×</button>`}
9437 </div>
9438 </div>
9439 ` : html``}
9440 <div class="body">
9441 <slot></slot>
9442 </div>
9443 <div class="footer">
9444 <slot name="footer"></slot>
9445 </div>
9446 </div>
9447 `;
9448 }
9449 };
9450 _WpdModal.props = ["open", "title", "size", "mandatory"];
9451 _WpdModal.styles = [modalStyles];
9452 _WpdModal.help = {
9453 title: "Modal overlay",
9454 summary: "Overlay container with title, body, and footer slots. Handles ESC, click-outside, focus trap. Use for rich modal flows that go beyond a yes/no confirm. The dialog surface is dark and re-points the shared surface tokens (--desktop-mode-text/-muted/-border/-window-bg, --wpd-button-bg-hover) so wpd-* controls slotted into it resolve readable dark-surface colors automatically.",
9455 status: "experimental",
9456 since: "0.8.5",
9457 props: [
9458 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
9459 { name: "title", type: "string", description: "Heading shown at the top of the dialog." },
9460 { name: "size", type: "'sm' | 'md' | 'lg'", default: "md", description: "Width preset." },
9461 {
9462 name: "mandatory",
9463 type: "boolean attribute",
9464 description: "Disables ESC, click-outside and the close button."
9465 }
9466 ],
9467 slots: [
9468 { name: "(default)", description: "Body content." },
9469 { name: "footer", description: "Footer button row, right-aligned." },
9470 { name: "header-actions", description: "Extra actions next to the close button." }
9471 ],
9472 events: [
9473 {
9474 name: "wpd-modal-cancel",
9475 description: "Fires when the user dismisses the modal (ESC, click-outside, close button). Cancelable; calling `preventDefault()` keeps the modal open."
9476 }
9477 ]
9478 };
9479 let WpdModal = _WpdModal;
9480 defineComponent("wpd-modal", WpdModal);
9481 const styles$6 = css`:host{display:inline-flex}:host( [ fill-cell ] ){display:flex;width:100%}button{appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:var( --wpd-button-padding,6px 12px );border-radius:var( --wpd-button-border-radius,6px );font:inherit;font-weight:500;cursor:pointer;transition:background-color 0.12s ease,color 0.12s ease,border-color 0.12s ease;background:var( --wpd-button-bg,transparent );color:var( --wpd-button-fg,var( --desktop-mode-text,#1d2327 ) );border:var( --wpd-button-border,1px solid var( --desktop-mode-border,#c3c4c7 ) )}:host( [ fill-cell ] ) button{width:100%;min-height:var( --wpd-button-min-height,44px )}button:disabled{opacity:0.5;cursor:not-allowed}button:hover:not(:disabled ){background:var( --wpd-button-bg-hover,rgba( 0,0,0,0.04 ) )}:host( [ variant='primary' ] ) button{background:var( --wpd-button-bg,var( --wp-admin-theme-color,#2271b1 ) );color:var( --wpd-button-fg,#fff );border:var( --wpd-button-border,1px solid transparent )}:host( [ variant='primary' ] ) button:hover:not(:disabled ){filter:brightness( 1.06 );background:var( --wpd-button-bg,var( --wp-admin-theme-color,#2271b1 ) )}:host( [ variant='secondary' ] ) button{background:var( --wpd-button-bg,rgba( 0,0,0,0.06 ) );color:var( --wpd-button-fg,var( --desktop-mode-text,#1d2327 ) );border:var( --wpd-button-border,1px solid transparent )}:host( [ variant='secondary' ] ) button:hover:not(:disabled ){background:var( --wpd-button-bg-hover,rgba( 0,0,0,0.1 ) )}:host( [ variant='danger' ] ) button{background:var( --wpd-button-bg,transparent );color:var( --wpd-button-fg,#d63638 );border:var( --wpd-button-border,1px solid currentColor )}:host( [ variant='danger' ] ) button:hover:not(:disabled ){background:#d63638;color:#fff}:host( [ variant='link' ] ) button{background:transparent;color:var( --wpd-button-fg,var( --wp-admin-theme-color,#2271b1 ) );border:0;padding:0;text-decoration:underline}:host( [ busy ] ) button{pointer-events:none;opacity:0.75}.wpd-button__spinner{box-sizing:border-box;display:inline-block;width:12px;height:12px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:wpd-button-spin 0.6s linear infinite;flex-shrink:0}@keyframes wpd-button-spin{to{transform:rotate( 360deg )}}`;
9482 const _WpdButton = class _WpdButton extends Component {
9483 render() {
9484 const disabled = this.disabled !== null;
9485 const busy = this.busy !== null;
9486 const type = this.type || "button";
9487 return html`
9488 <button
9489 part="button"
9490 type=${type}
9491 ?disabled=${disabled || busy}
9492 aria-busy=${busy ? "true" : "false"}
9493 >
9494 ${busy ? html`<span class="wpd-button__spinner" aria-hidden="true"></span>` : ""}
9495 <slot></slot>
9496 </button>
9497 `;
9498 }
9499 };
9500 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
9501 _WpdButton.styles = [styles$6];
9502 _WpdButton.help = {
9503 title: "Button",
9504 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
9505 status: "stable",
9506 since: "0.9.0",
9507 props: [
9508 {
9509 name: "variant",
9510 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
9511 default: "ghost",
9512 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
9513 },
9514 {
9515 name: "disabled",
9516 type: "boolean attribute",
9517 description: "Disable pointer + keyboard interaction and dim the chrome."
9518 },
9519 {
9520 name: "type",
9521 type: "'button' | 'submit' | 'reset'",
9522 default: "button",
9523 description: "Forwarded to the underlying native <button>."
9524 },
9525 {
9526 name: "busy",
9527 type: "boolean attribute",
9528 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
9529 },
9530 {
9531 name: "fill-cell",
9532 type: "boolean attribute",
9533 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
9534 }
9535 ],
9536 slots: [{ name: "(default)", description: "Button label." }],
9537 parts: [{ name: "button", description: "Underlying <button> element." }],
9538 cssProps: [
9539 { name: "--wpd-button-bg", description: "Background color." },
9540 {
9541 name: "--wpd-button-bg-hover",
9542 description: "Hover wash (ghost + secondary variants)."
9543 },
9544 { name: "--wpd-button-fg", description: "Text color." },
9545 { name: "--wpd-button-border", description: "Border shorthand." },
9546 { name: "--wpd-button-border-radius", default: "6px" },
9547 { name: "--wpd-button-padding", default: "6px 12px" },
9548 {
9549 name: "--wpd-button-min-height",
9550 description: "Minimum height when fill-cell is set."
9551 }
9552 ],
9553 example: html`
9554 <wpd-cluster gap="8">
9555 <wpd-button variant="primary">Primary</wpd-button>
9556 <wpd-button variant="secondary">Secondary</wpd-button>
9557 <wpd-button variant="ghost">Ghost</wpd-button>
9558 <wpd-button variant="danger">Danger</wpd-button>
9559 <wpd-button variant="link">Link</wpd-button>
9560 </wpd-cluster>
9561 `
9562 };
9563 let WpdButton = _WpdButton;
9564 defineComponent("wpd-button", WpdButton);
9565 function customGradientCss(state2) {
9566 const { from, to, angle } = state2.customGradient;
9567 return `linear-gradient(${angle}deg, ${from}, ${to})`;
9568 }
9569 const CUSTOM_GRADIENT_DESCRIPTION = () => __("Mix your own two-colour gradient and set the angle — your desk, your palette.");
9570 function registerCustomGradient(ctx) {
9571 register$3({
9572 id: CUSTOM_GRADIENT_ID,
9573 label: __("Custom gradient"),
9574 type: "css",
9575 preview: customGradientCss(ctx.state),
9576 description: CUSTOM_GRADIENT_DESCRIPTION(),
9577 resolveValue: () => customGradientCss(ctx.state)
9578 });
9579 }
9580 function registerCustomImageIfPresent(state2) {
9581 if (!state2.customImage) {
9582 unregister$3(CUSTOM_IMAGE_ID);
9583 return;
9584 }
9585 const safeUrl = encodeURI(state2.customImage.url);
9586 const value = `url("${safeUrl}") center/cover no-repeat, #1d2327`;
9587 register$3({
9588 id: CUSTOM_IMAGE_ID,
9589 label: __("Custom image"),
9590 type: "css",
9591 value,
9592 preview: value,
9593 description: __(
9594 "Any image from your media library or an upload, sized to cover the whole desk."
9595 )
9596 });
9597 }
9598 let _panelLoadPromise = null;
9599 function loadOsSettingsPanelBundle(scriptUrl) {
9600 if (window.desktopModeRenderOsSettingsPanel) {
9601 return Promise.resolve(window.desktopModeRenderOsSettingsPanel);
9602 }
9603 if (_panelLoadPromise) {
9604 return _panelLoadPromise;
9605 }
9606 _panelLoadPromise = new Promise((resolve2, reject) => {
9607 const existing = document.querySelector(
9608 'script[data-desktop-mode-os-settings-panel="1"]'
9609 );
9610 const finish = () => {
9611 const fn = window.desktopModeRenderOsSettingsPanel;
9612 if (!fn) {
9613 reject(
9614 new Error(
9615 "[desktop-mode] os-settings-panel bundle loaded but did not register desktopModeRenderOsSettingsPanel"
9616 )
9617 );
9618 return;
9619 }
9620 resolve2(fn);
9621 };
9622 if (existing) {
9623 if (window.desktopModeRenderOsSettingsPanel) {
9624 finish();
9625 } else {
9626 existing.addEventListener("load", finish);
9627 existing.addEventListener(
9628 "error",
9629 () => reject(new Error("failed to load os-settings-panel bundle"))
9630 );
9631 }
9632 return;
9633 }
9634 const s = document.createElement("script");
9635 s.src = scriptUrl;
9636 s.async = true;
9637 s.dataset.desktopModeOsSettingsPanel = "1";
9638 s.addEventListener("load", finish);
9639 s.addEventListener(
9640 "error",
9641 () => reject(new Error("failed to load os-settings-panel bundle"))
9642 );
9643 document.head.appendChild(s);
9644 });
9645 return _panelLoadPromise;
9646 }
9647 class OsSettings {
9648 constructor(config, layer) {
9649 this.activeEditorTeardown = null;
9650 this.tabRegistryUnsubscribe = null;
9651 this.activeTabId = null;
9652 this.osSettingsListeners = /* @__PURE__ */ new Set();
9653 this._lastRenderedBody = null;
9654 this.config = config;
9655 this.layer = layer;
9656 this.state = loadState();
9657 setLastConfirmedState(this.state);
9658 document.addEventListener(
9659 "desktop-mode-os-settings-save-lifecycle",
9660 (e) => {
9661 const detail = e.detail;
9662 if (!detail || detail.phase !== "failed" || !detail.rolledBackTo) {
9663 return;
9664 }
9665 this.state = detail.rolledBackTo;
9666 this.apply();
9667 if (this._lastRenderedBody?.isConnected) {
9668 this.renderPanel(this._lastRenderedBody);
9669 }
9670 }
9671 );
9672 registerCustomGradient(this);
9673 registerCustomImageIfPresent(this.state);
9674 }
9675 /** Project the private state into the public snapshot shape. */
9676 getOsSettingsSnapshot() {
9677 return {
9678 wallpaper: this.state.wallpaper,
9679 accent: this.state.accent,
9680 dockSize: this.state.dockSize,
9681 desktopLayout: this.state.desktopLayout,
9682 dockRailRenderer: this.state.dockRailRenderer,
9683 unfocusEffect: this.state.unfocusEffect,
9684 windowLinkRenderer: this.state.windowLinkRenderer,
9685 windowLinkVisibility: this.state.windowLinkVisibility,
9686 windowLinksEnabled: this.state.windowLinksEnabled,
9687 windowLinkRaiseOnFocus: this.state.windowLinkRaiseOnFocus,
9688 windowLinkHighlight: this.state.windowLinkHighlight,
9689 ai: { ...this.state.ai },
9690 nativePostsEnabled: this.state.nativePostsEnabled,
9691 nativePostsHiddenColumns: this.state.nativePostsHiddenColumns.slice(),
9692 nativePagesEnabled: this.state.nativePagesEnabled,
9693 nativeUsersEnabled: this.state.nativeUsersEnabled,
9694 nativePluginsEnabled: this.state.nativePluginsEnabled,
9695 nativeCommentsEnabled: this.state.nativeCommentsEnabled,
9696 developerModeEnabled: this.state.developerModeEnabled,
9697 foldersSharingEnabled: this.state.foldersSharingEnabled,
9698 itemVisibility: { ...this.state.itemVisibility },
9699 dockOrder: this.state.dockOrder.slice(),
9700 dockPromotedPositions: Object.fromEntries(
9701 Object.entries(this.state.dockPromotedPositions).map(
9702 ([k, v]) => [k, { ...v }]
9703 )
9704 )
9705 };
9706 }
9707 subscribeOsSettings(cb) {
9708 this.osSettingsListeners.add(cb);
9709 return () => {
9710 this.osSettingsListeners.delete(cb);
9711 };
9712 }
9713 /**
9714 * Apply the current state: wallpaper via the layer, accent + dock
9715 * size as CSS custom properties on the shell.
9716 *
9717 * Safe to call repeatedly — calls into `layer.apply` dedupe via
9718 * generation counter; CSS property writes are idempotent.
9719 */
9720 apply() {
9721 const shell = document.getElementById("desktop-mode-shell");
9722 if (!shell) {
9723 return;
9724 }
9725 seedWallpaperSettings(this.state.wallpaperSettings);
9726 const def = get$2(this.state.wallpaper) || get$2(getDefaultWallpaperId()) || get$2(DEFAULT_WALLPAPER_ID) || all$2()[0];
9727 if (def) {
9728 this.layer.apply(def);
9729 }
9730 const accents = getAccents();
9731 const accent = accents.find((a) => a.id === this.state.accent) ?? accents[0];
9732 const dockSize = DOCK_SIZES.find((d) => d.id === this.state.dockSize) ?? DOCK_SIZES[1];
9733 const root = document.documentElement;
9734 root.style.setProperty("--wp-admin-theme-color", accent.value);
9735 root.style.setProperty("--desktop-mode-dock-width", `${dockSize.width}px`);
9736 root.style.setProperty("--desktop-mode-dock-icon-size", `${dockSize.icon}px`);
9737 shell.setAttribute(
9738 "data-desktop-mode-layout",
9739 this.state.desktopLayout
9740 );
9741 setActiveRenderer(this.state.dockRailRenderer);
9742 }
9743 save(opts = {}) {
9744 saveState(this.state, opts);
9745 if (this.osSettingsListeners.size > 0) {
9746 const snapshot = this.getOsSettingsSnapshot();
9747 const listeners2 = Array.from(this.osSettingsListeners);
9748 for (const cb of listeners2) {
9749 try {
9750 cb(snapshot);
9751 } catch (err) {
9752 if (typeof console !== "undefined") {
9753 console.error(
9754 "[desktop-mode] os-settings listener threw:",
9755 err
9756 );
9757 }
9758 }
9759 }
9760 }
9761 }
9762 /**
9763 * Render the settings panel into the given native-window body.
9764 *
9765 * Builds three sections (wallpaper, accent, dock size) and wires
9766 * each to save/apply on change. The panel is a one-shot build per
9767 * window open — closing and re-opening renders a fresh tree.
9768 */
9769 /**
9770 * Render the settings panel into the given native-window body.
9771 *
9772 * Lazy since 0.8.4 — the actual rendering logic plus every
9773 * `<wpd-*>` component the panel uses lives in
9774 * `src/settings/panel.ts`, compiled into its own Vite target
9775 * `os-settings-panel[.min].js`. The script is injected on the
9776 * first call below and the matching
9777 * `window.desktopModeRenderOsSettingsPanel( ctx, body )` global
9778 * is then invoked. Subsequent calls (registry-driven re-render,
9779 * save-failure rollback) skip the load and forward immediately.
9780 *
9781 * Why this is a `<script>`-injected sibling bundle rather than
9782 * an in-bundle dynamic import: Vite IIFE lib mode inlines
9783 * `import()` calls, so an in-bundle lazy import would give zero
9784 * byte savings. A separate Vite target is the only mechanism
9785 * that actually shrinks `desktop.min.js`. See the Stage 8
9786 * section of `BUNDLE-SIZE-REPORT.md` for the full picture.
9787 */
9788 /**
9789 * Switch the active settings tab. Records the choice on
9790 * {@link activeTabId} (so the next render mounts on it) and, when
9791 * the panel is currently mounted, flips the live `<wpd-tabs>` value
9792 * in place so an already-open OS Settings window jumps to the tab
9793 * without a full re-render. Deep-linking entry points
9794 * (`openOsSettings({ tabId })`) call this after opening the window.
9795 *
9796 * @param tabId Settings tab id, e.g. `'ai'`, `'apps-icons'`.
9797 */
9798 focusTab(tabId) {
9799 this.activeTabId = tabId;
9800 const body = this._lastRenderedBody;
9801 if (!body?.isConnected) {
9802 return;
9803 }
9804 const tabs = body.querySelector("wpd-tabs");
9805 if (tabs) {
9806 tabs.value = tabId;
9807 }
9808 }
9809 renderPanel(body) {
9810 this._lastRenderedBody = body;
9811 const fn = window.desktopModeRenderOsSettingsPanel;
9812 if (fn) {
9813 fn(this, body);
9814 return;
9815 }
9816 void loadOsSettingsPanelBundle(
9817 this.config.osSettingsPanelBundleUrl ?? ""
9818 ).then((render2) => {
9819 if (!body.isConnected) {
9820 return;
9821 }
9822 render2(this, body);
9823 }).catch((err) => {
9824 if (typeof console !== "undefined") {
9825 console.error(
9826 "[desktop-mode] OS Settings panel failed to load:",
9827 err
9828 );
9829 }
9830 });
9831 }
9832 }
9833 const EXIT_DESKTOP_MODE_TILE_ID = "desktop-mode-exit";
9834 function getExitDesktopModeTileDef() {
9835 return {
9836 id: EXIT_DESKTOP_MODE_TILE_ID,
9837 title: __("Exit Desktop Mode"),
9838 // `dashicons-exit` (door with arrow) is the clearest "leave"
9839 // glyph in the WordPress set, distinct from `dashicons-desktop`
9840 // used by OS Settings.
9841 icon: "dashicons-exit",
9842 onOpen: () => {
9843 void exitDesktopMode();
9844 }
9845 };
9846 }
9847 async function exitDesktopMode() {
9848 const cfg = window.desktopModeAdminBar;
9849 const fallback = cfg?.classicUrl || "/wp-admin/";
9850 if (!cfg?.ajaxUrl || !cfg?.nonce) {
9851 navigateTop(fallback);
9852 return;
9853 }
9854 const body = new URLSearchParams();
9855 body.set("action", "save-desktop-mode");
9856 body.set("nonce", cfg.nonce);
9857 body.set("enabled", "");
9858 let target2 = fallback;
9859 try {
9860 const res = await fetch(cfg.ajaxUrl, {
9861 method: "POST",
9862 headers: {
9863 "Content-Type": "application/x-www-form-urlencoded"
9864 },
9865 body: body.toString(),
9866 credentials: "same-origin"
9867 });
9868 if (res.ok) {
9869 const json = await res.json();
9870 if (json?.success && json.data?.redirect) {
9871 target2 = json.data.redirect;
9872 }
9873 }
9874 } catch {
9875 }
9876 navigateTop(target2);
9877 }
9878 function navigateTop(url) {
9879 try {
9880 window.top.location.href = url;
9881 } catch {
9882 window.location.href = url;
9883 }
9884 }
9885 const _initial$1 = {
9886 userId: null,
9887 requestedAt: 0,
9888 tabRequested: false
9889 };
9890 let _store$2 = null;
9891 function getStore$1() {
9892 if (_store$2) {
9893 return _store$2;
9894 }
9895 const w = window;
9896 const factory = w.wp?.desktop?.createSharedStore;
9897 if (typeof factory !== "function") {
9898 return null;
9899 }
9900 _store$2 = factory(
9901 "desktop-mode/user-edit/target",
9902 () => ({ ..._initial$1 })
9903 );
9904 return _store$2;
9905 }
9906 function setUserEditTarget(userId) {
9907 const store2 = getStore$1();
9908 if (store2) {
9909 store2.state.userId = userId;
9910 store2.state.requestedAt = Date.now();
9911 store2.state.tabRequested = true;
9912 store2.notify();
9913 return;
9914 }
9915 const w = window;
9916 w._wpdUserEditTarget = {
9917 userId,
9918 requestedAt: Date.now(),
9919 tabRequested: true
9920 };
9921 }
9922 const pending = /* @__PURE__ */ new Map();
9923 function loadVendorScript(url, extras) {
9924 const existing = pending.get(url);
9925 if (existing) {
9926 return existing;
9927 }
9928 const promise = new Promise((resolve2, reject) => {
9929 const selector = `script[data-desktop-mode-vendor="${cssEscape(url)}"]`;
9930 const preexisting = document.querySelector(selector);
9931 if (preexisting) {
9932 if (preexisting.dataset.loaded === "1") {
9933 resolve2();
9934 return;
9935 }
9936 preexisting.addEventListener("load", () => resolve2(), { once: true });
9937 preexisting.addEventListener(
9938 "error",
9939 () => reject(new Error(`Failed to load ${url}`)),
9940 { once: true }
9941 );
9942 return;
9943 }
9944 if (extras?.translations) {
9945 injectInline(extras.translations);
9946 }
9947 for (const code of extras?.l10n ?? []) {
9948 injectInline(code);
9949 }
9950 for (const code of extras?.before ?? []) {
9951 injectInline(code);
9952 }
9953 const script = document.createElement("script");
9954 script.src = url;
9955 script.async = true;
9956 script.dataset.desktopModeVendor = url;
9957 script.addEventListener(
9958 "load",
9959 () => {
9960 script.dataset.loaded = "1";
9961 for (const code of extras?.after ?? []) {
9962 injectInline(code);
9963 }
9964 resolve2();
9965 },
9966 { once: true }
9967 );
9968 script.addEventListener(
9969 "error",
9970 () => {
9971 pending.delete(url);
9972 script.remove();
9973 reject(new Error(`Failed to load ${url}`));
9974 },
9975 { once: true }
9976 );
9977 document.head.appendChild(script);
9978 });
9979 pending.set(url, promise);
9980 return promise;
9981 }
9982 function injectInline(code) {
9983 if (!code) {
9984 return;
9985 }
9986 const tag = document.createElement("script");
9987 tag.textContent = code;
9988 tag.dataset.desktopModeVendorInline = "1";
9989 document.head.appendChild(tag);
9990 }
9991 function cssEscape(value) {
9992 if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
9993 return CSS.escape(value);
9994 }
9995 return value.replace(/["\\]/g, "\\$&");
9996 }
9997 const registry$9 = /* @__PURE__ */ new Map();
9998 function registerModule(def) {
9999 if (!def || typeof def.id !== "string" || def.id === "") {
10000 if (typeof console !== "undefined") {
10001 console.warn("[desktop-mode] Ignored invalid module registration:", def);
10002 }
10003 return;
10004 }
10005 if (typeof def.url !== "string" || def.url === "") {
10006 if (typeof console !== "undefined") {
10007 console.warn(
10008 `[desktop-mode] Module "${def.id}" has no url; ignored.`
10009 );
10010 }
10011 return;
10012 }
10013 registry$9.set(def.id, def);
10014 }
10015 function moduleIds() {
10016 return Array.from(registry$9.keys());
10017 }
10018 async function loadModules(ids) {
10019 if (!ids || ids.length === 0) {
10020 return;
10021 }
10022 const unknown = ids.filter((id) => !registry$9.has(id));
10023 if (unknown.length > 0) {
10024 throw new Error(
10025 `[desktop-mode] Unknown module(s) in needs: ${unknown.map((id) => `"${id}"`).join(", ")}. Known modules: ${moduleIds().join(", ") || "(none)"}.`
10026 );
10027 }
10028 await Promise.all(
10029 ids.map((id) => {
10030 const def = registry$9.get(id);
10031 if (!def) {
10032 return Promise.resolve();
10033 }
10034 if (def.isReady && def.isReady()) {
10035 return Promise.resolve();
10036 }
10037 return loadVendorScript(def.url);
10038 })
10039 );
10040 }
10041 function createContext(id, pluginUrl) {
10042 return {
10043 id,
10044 pluginUrl,
10045 prefersReducedMotion: prefersReducedMotion$1(),
10046 visible: !document.hidden,
10047 settings: getWallpaperSettings(id)
10048 };
10049 }
10050 function prefersReducedMotion$1() {
10051 if (typeof window.matchMedia !== "function") {
10052 return false;
10053 }
10054 return window.matchMedia("( prefers-reduced-motion: reduce )").matches;
10055 }
10056 class WallpaperLayer {
10057 constructor(element, pluginUrl) {
10058 this.generation = 0;
10059 this.active = null;
10060 this.suspendReasons = /* @__PURE__ */ new Map();
10061 this.freezeOverlay = null;
10062 this.frozenCanvas = null;
10063 this.boundVisibilityChange = () => {
10064 this.emitEffectiveVisibility();
10065 };
10066 this.element = element;
10067 this.pluginUrl = pluginUrl;
10068 document.addEventListener("visibilitychange", this.boundVisibilityChange);
10069 }
10070 /**
10071 * Apply a wallpaper definition. Safe to call from any event
10072 * handler — handles type dispatch, teardown of the prior active
10073 * canvas, and race-safe async mounts.
10074 */
10075 apply(def) {
10076 const gen = ++this.generation;
10077 this.teardownActive();
10078 if (def.type === "css") {
10079 this.applyCss(def);
10080 return;
10081 }
10082 this.applyCanvas(def, gen);
10083 }
10084 /**
10085 * Suspend wallpaper animation — e.g. while a game renders its own
10086 * canvas. Refcounted per reason; the wallpaper stays suspended
10087 * until every held reason is resumed. On the first held reason the
10088 * layer freezes the current frame into a bitmap overlay
10089 * (best-effort) and re-emits the effective visibility so mounted
10090 * scenes stop their tickers. The scene is never destroyed.
10091 */
10092 suspend(reason) {
10093 const wasSuspended = this.isSuspended();
10094 this.suspendReasons.set(
10095 reason,
10096 (this.suspendReasons.get(reason) ?? 0) + 1
10097 );
10098 if (wasSuspended) {
10099 return;
10100 }
10101 this.installFreezeOverlay();
10102 this.emitSuspendAction();
10103 this.emitEffectiveVisibility();
10104 }
10105 /**
10106 * Release one hold on a suspend reason. Animation resumes once no
10107 * reason remains held. Unknown reasons are ignored.
10108 */
10109 resume(reason) {
10110 const count = this.suspendReasons.get(reason);
10111 if (count === void 0) {
10112 return;
10113 }
10114 if (count > 1) {
10115 this.suspendReasons.set(reason, count - 1);
10116 return;
10117 }
10118 this.suspendReasons.delete(reason);
10119 if (this.isSuspended()) {
10120 return;
10121 }
10122 this.removeFreezeOverlay();
10123 this.emitSuspendAction();
10124 this.emitEffectiveVisibility();
10125 }
10126 /** Whether any suspend reason is currently held. */
10127 isSuspended() {
10128 return this.suspendReasons.size > 0;
10129 }
10130 /**
10131 * Imperative teardown entry point — called from desktop.ts on
10132 * `pagehide` so a canvas wallpaper's ticker doesn't compete with
10133 * the session-beacon flush at unload.
10134 */
10135 teardownActive() {
10136 this.removeFreezeOverlay();
10137 if (!this.active) {
10138 return;
10139 }
10140 const { id, teardown } = this.active;
10141 this.active = null;
10142 doAction(HOOKS.WALLPAPER_UNMOUNTING, { id });
10143 try {
10144 teardown();
10145 } catch (err) {
10146 doAction(HOOKS.SHELL_ERROR, { scope: "wallpaper-teardown", id, error: err });
10147 if (typeof console !== "undefined") {
10148 console.error(
10149 `[desktop-mode] Wallpaper "${id}" teardown threw:`,
10150 err
10151 );
10152 }
10153 }
10154 this.element.innerHTML = "";
10155 }
10156 /** Remove listeners. Not called in normal flow — reserved for tests. */
10157 dispose() {
10158 this.teardownActive();
10159 document.removeEventListener("visibilitychange", this.boundVisibilityChange);
10160 }
10161 applyCss(def) {
10162 const value = def.resolveValue ? def.resolveValue(createContext(def.id, this.pluginUrl)) : def.value;
10163 if (typeof value === "string") {
10164 this.element.style.setProperty("--desktop-mode-bg", value);
10165 const shell = document.getElementById("desktop-mode-shell");
10166 shell?.style.setProperty("--desktop-mode-bg", value);
10167 }
10168 }
10169 applyCanvas(def, gen) {
10170 const ctx = createContext(def.id, this.pluginUrl);
10171 doAction(HOOKS.WALLPAPER_MOUNTING, { id: def.id, container: this.element, ctx });
10172 const depsReady = def.needs && def.needs.length > 0 ? loadModules(def.needs) : Promise.resolve();
10173 const onResolve = (teardown) => {
10174 if (gen !== this.generation) {
10175 try {
10176 teardown();
10177 } catch {
10178 }
10179 return;
10180 }
10181 this.active = { id: def.id, teardown };
10182 doAction(HOOKS.WALLPAPER_MOUNTED, { id: def.id, container: this.element, ctx });
10183 if (this.isEffectivelyHidden()) {
10184 this.emitEffectiveVisibility();
10185 }
10186 };
10187 depsReady.then(
10188 () => {
10189 if (gen !== this.generation) {
10190 return;
10191 }
10192 let result;
10193 try {
10194 result = def.mount(this.element, ctx);
10195 } catch (err) {
10196 this.handleMountFailure(def.id, err);
10197 return;
10198 }
10199 if (isThenable$1(result)) {
10200 result.then(onResolve, (err) => {
10201 if (gen !== this.generation) {
10202 return;
10203 }
10204 this.handleMountFailure(def.id, err);
10205 });
10206 return;
10207 }
10208 onResolve(result);
10209 },
10210 (err) => {
10211 if (gen !== this.generation) {
10212 return;
10213 }
10214 this.handleMountFailure(def.id, err);
10215 }
10216 );
10217 }
10218 /** Hidden tab OR held suspend reason — what mounted scenes act on. */
10219 isEffectivelyHidden() {
10220 return document.hidden || this.isSuspended();
10221 }
10222 /**
10223 * Re-emit `WALLPAPER_VISIBILITY` with the effective state. Both the
10224 * `visibilitychange` listener and suspend/resume route through this,
10225 * so a tab re-focus during suspension cannot restart animation.
10226 */
10227 emitEffectiveVisibility() {
10228 if (!this.active) {
10229 return;
10230 }
10231 doAction(HOOKS.WALLPAPER_VISIBILITY, {
10232 id: this.active.id,
10233 state: this.isEffectivelyHidden() ? "hidden" : "visible"
10234 });
10235 }
10236 emitSuspendAction() {
10237 doAction(HOOKS.WALLPAPER_SUSPEND, {
10238 id: this.active?.id ?? null,
10239 suspended: this.isSuspended(),
10240 reasons: Array.from(this.suspendReasons.keys())
10241 });
10242 }
10243 /**
10244 * Freeze the current frame: copy the live wallpaper canvas onto a
10245 * 2D overlay canvas layered above it, then hide the live canvas.
10246 * Best-effort — Pixi's WebGL canvas has no `preserveDrawingBuffer`,
10247 * so the draw can produce a blank on some drivers; on any failure
10248 * we skip the overlay entirely (a canvas whose ticker stops keeps
10249 * presenting its last frame anyway).
10250 */
10251 installFreezeOverlay() {
10252 if (this.freezeOverlay || !this.active) {
10253 return;
10254 }
10255 const source = this.element.querySelector("canvas");
10256 if (!source || source.width === 0 || source.height === 0) {
10257 return;
10258 }
10259 try {
10260 const overlay = document.createElement("canvas");
10261 overlay.width = source.width;
10262 overlay.height = source.height;
10263 const ctx2d = overlay.getContext("2d");
10264 if (!ctx2d) {
10265 return;
10266 }
10267 ctx2d.drawImage(source, 0, 0);
10268 overlay.className = "desktop-mode-wallpaper-freeze";
10269 overlay.style.position = "absolute";
10270 overlay.style.inset = "0";
10271 overlay.style.width = "100%";
10272 overlay.style.height = "100%";
10273 overlay.style.pointerEvents = "none";
10274 overlay.setAttribute("aria-hidden", "true");
10275 this.element.appendChild(overlay);
10276 source.style.visibility = "hidden";
10277 this.freezeOverlay = overlay;
10278 this.frozenCanvas = source;
10279 } catch {
10280 }
10281 }
10282 removeFreezeOverlay() {
10283 this.freezeOverlay?.remove();
10284 this.freezeOverlay = null;
10285 if (this.frozenCanvas) {
10286 this.frozenCanvas.style.visibility = "";
10287 this.frozenCanvas = null;
10288 }
10289 }
10290 handleMountFailure(id, err) {
10291 this.element.innerHTML = "";
10292 doAction(HOOKS.WALLPAPER_MOUNT_FAILED, { id, error: err });
10293 doAction(HOOKS.SHELL_ERROR, { scope: "wallpaper-mount", id, error: err });
10294 if (typeof console !== "undefined") {
10295 console.error(
10296 `[desktop-mode] Wallpaper "${id}" failed to mount:`,
10297 err
10298 );
10299 }
10300 }
10301 }
10302 function isThenable$1(value) {
10303 return !!value && typeof value === "object" && typeof value.then === "function";
10304 }
10305 function createWallpaperRegistrySync(deps2) {
10306 const { osSettings } = deps2;
10307 const registered = /* @__PURE__ */ new Set();
10308 const loadedScripts = /* @__PURE__ */ new Set();
10309 const ensureScript = async (entry) => {
10310 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
10311 return;
10312 }
10313 try {
10314 await loadVendorScript(entry.scriptUrl, {
10315 translations: entry.scriptTranslations,
10316 l10n: entry.scriptL10n,
10317 before: entry.scriptBefore,
10318 after: entry.scriptAfter
10319 });
10320 } catch (err) {
10321 doAction(HOOKS.SHELL_ERROR, {
10322 scope: "wallpaper-script-load",
10323 id: entry.id,
10324 error: err
10325 });
10326 return;
10327 }
10328 loadedScripts.add(entry.scriptUrl);
10329 };
10330 const readDef = (id) => {
10331 const globals = window.desktopModeWallpapers || {};
10332 return globals[id] ?? null;
10333 };
10334 const defFromCssEntry = (entry) => {
10335 if (entry.type !== "css" || entry.value === "") {
10336 return null;
10337 }
10338 return {
10339 id: entry.id,
10340 label: entry.label,
10341 type: "css",
10342 value: entry.value,
10343 preview: entry.preview !== "" ? entry.preview : entry.value,
10344 description: entry.description || void 0
10345 };
10346 };
10347 const registerEntry = async (entry) => {
10348 if (registered.has(entry.id)) {
10349 return;
10350 }
10351 const cssDef = defFromCssEntry(entry);
10352 if (cssDef) {
10353 register$3(cssDef);
10354 registered.add(entry.id);
10355 osSettings.apply();
10356 return;
10357 }
10358 await ensureScript(entry);
10359 let def = readDef(entry.id);
10360 if (def && !def.description && entry.description) {
10361 def = { ...def, description: entry.description };
10362 }
10363 if (!def) {
10364 doAction(HOOKS.SHELL_ERROR, {
10365 scope: "wallpaper-missing-def",
10366 id: entry.id,
10367 error: new Error(
10368 `[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.`
10369 )
10370 });
10371 return;
10372 }
10373 try {
10374 register$3(def);
10375 } catch (err) {
10376 doAction(HOOKS.SHELL_ERROR, {
10377 scope: "wallpaper-register",
10378 id: entry.id,
10379 error: err
10380 });
10381 return;
10382 }
10383 registered.add(entry.id);
10384 osSettings.apply();
10385 };
10386 const unregisterEntry = (id) => {
10387 if (!registered.has(id)) {
10388 return;
10389 }
10390 unregister$3(id);
10391 registered.delete(id);
10392 osSettings.apply();
10393 };
10394 return async (list2) => {
10395 const incoming = /* @__PURE__ */ new Set();
10396 for (const entry of list2) {
10397 incoming.add(entry.id);
10398 }
10399 for (const id of Array.from(registered)) {
10400 if (!incoming.has(id)) {
10401 unregisterEntry(id);
10402 }
10403 }
10404 for (const entry of list2) {
10405 if (!registered.has(entry.id)) {
10406 await registerEntry(entry);
10407 }
10408 }
10409 };
10410 }
10411 const store$e = createSharedStore(
10412 "desktop-mode/games-registry",
10413 () => ({
10414 seed: [],
10415 listeners: /* @__PURE__ */ new Set()
10416 })
10417 );
10418 const seed$3 = store$e.state.seed;
10419 const listeners$c = store$e.state.listeners;
10420 function register$1(entry) {
10421 throwOnRegistrationErrors(
10422 "Game",
10423 collectRegistrationErrors(entry, GAME_CHECKS),
10424 entry
10425 );
10426 const idx = seed$3.findIndex((g) => g.id === entry.id);
10427 if (idx >= 0) {
10428 seed$3[idx] = entry;
10429 } else {
10430 seed$3.push(entry);
10431 }
10432 notify$f();
10433 }
10434 function unregister$1(id) {
10435 const idx = seed$3.findIndex((g) => g.id === id);
10436 if (idx >= 0) {
10437 seed$3.splice(idx, 1);
10438 notify$f();
10439 }
10440 }
10441 function subscribe$3(cb) {
10442 listeners$c.add(cb);
10443 return () => {
10444 listeners$c.delete(cb);
10445 };
10446 }
10447 function notify$f() {
10448 const snapshot = Array.from(listeners$c);
10449 for (const cb of snapshot) {
10450 try {
10451 cb();
10452 } catch (err) {
10453 if (typeof console !== "undefined") {
10454 console.error(
10455 "[desktop-mode] games registry listener threw:",
10456 err
10457 );
10458 }
10459 }
10460 }
10461 }
10462 function all$1() {
10463 const copy = seed$3.slice();
10464 const filtered = applyFilters(HOOKS.GAMES, copy);
10465 if (!Array.isArray(filtered)) {
10466 if (typeof console !== "undefined") {
10467 console.warn(
10468 "[desktop-mode] `desktop-mode.games` filter returned a non-array; falling back to seed list."
10469 );
10470 }
10471 return copy;
10472 }
10473 return filtered.filter(isValidEntry);
10474 }
10475 function get$1(id) {
10476 return all$1().find((g) => g.id === id);
10477 }
10478 const GAME_CHECKS = [
10479 {
10480 field: "id",
10481 message: "missing or not a non-empty string",
10482 valid: (g) => typeof g.id === "string" && g.id !== ""
10483 },
10484 {
10485 field: "title",
10486 message: "missing or not a non-empty string",
10487 valid: (g) => typeof g.title === "string" && g.title !== ""
10488 },
10489 {
10490 field: "scoreColumns",
10491 message: "must be an array",
10492 valid: (g) => Array.isArray(g.scoreColumns)
10493 },
10494 {
10495 field: "render/scriptUrl",
10496 message: "needs a `render` callback or a `scriptUrl` to lazily load one",
10497 valid: (g) => typeof g.render === "function" || typeof g.scriptUrl === "string" && g.scriptUrl !== ""
10498 }
10499 ];
10500 function isValidEntry(entry) {
10501 return collectRegistrationErrors(entry, GAME_CHECKS).length === 0;
10502 }
10503 function stubFromServerEntry(entry) {
10504 return {
10505 id: entry.id,
10506 title: entry.title,
10507 icon: entry.icon,
10508 description: entry.description || void 0,
10509 scoreColumns: Array.isArray(entry.scoreColumns) ? entry.scoreColumns : [],
10510 config: entry.config ?? {},
10511 scriptUrl: entry.scriptUrl,
10512 scriptTranslations: entry.scriptTranslations,
10513 scriptL10n: entry.scriptL10n,
10514 scriptBefore: entry.scriptBefore,
10515 scriptAfter: entry.scriptAfter
10516 };
10517 }
10518 function createGamesRegistrySync() {
10519 const registered = /* @__PURE__ */ new Set();
10520 const registerEntry = (entry) => {
10521 try {
10522 const existing = get$1(entry.id);
10523 const stub = stubFromServerEntry(entry);
10524 register$1(
10525 existing && typeof existing.render === "function" ? { ...stub, render: existing.render, window: existing.window } : stub
10526 );
10527 } catch (err) {
10528 if (typeof console !== "undefined") {
10529 console.error(
10530 `[desktop-mode] Server game "${entry.id}" failed to register:`,
10531 err
10532 );
10533 }
10534 return;
10535 }
10536 registered.add(entry.id);
10537 };
10538 const unregisterEntry = (id) => {
10539 if (!registered.has(id)) {
10540 return;
10541 }
10542 unregister$1(id);
10543 registered.delete(id);
10544 };
10545 return async (list2) => {
10546 const incoming = /* @__PURE__ */ new Set();
10547 for (const entry of list2) {
10548 if (entry && typeof entry.id === "string" && entry.id !== "") {
10549 incoming.add(entry.id);
10550 }
10551 }
10552 for (const id of Array.from(registered)) {
10553 if (!incoming.has(id)) {
10554 unregisterEntry(id);
10555 }
10556 }
10557 for (const entry of list2) {
10558 if (incoming.has(entry.id)) {
10559 registerEntry(entry);
10560 }
10561 }
10562 };
10563 }
10564 const suppliers = /* @__PURE__ */ new Map();
10565 const subscribers = /* @__PURE__ */ new Map();
10566 let booted$3 = false;
10567 const heartbeat = {
10568 contribute(field, supplier) {
10569 suppliers.set(field, supplier);
10570 return () => {
10571 if (suppliers.get(field) === supplier) {
10572 suppliers.delete(field);
10573 }
10574 };
10575 },
10576 subscribe(field, cb) {
10577 let set = subscribers.get(field);
10578 if (!set) {
10579 set = /* @__PURE__ */ new Set();
10580 subscribers.set(field, set);
10581 }
10582 set.add(cb);
10583 return () => {
10584 set.delete(cb);
10585 };
10586 }
10587 };
10588 function bootHeartbeatBus() {
10589 if (booted$3) {
10590 return;
10591 }
10592 booted$3 = true;
10593 const $ = window.jQuery;
10594 if (!$) {
10595 console.warn(
10596 "[desktop-mode/heartbeat] jQuery missing — Heartbeat bus disabled."
10597 );
10598 return;
10599 }
10600 $(document).on("heartbeat-send", (...args) => {
10601 const data = args[1];
10602 if (!data) {
10603 return;
10604 }
10605 for (const [field, supplier] of suppliers) {
10606 try {
10607 data[field] = supplier();
10608 } catch (err) {
10609 console.error(
10610 `[desktop-mode/heartbeat] supplier for "${field}" threw:`,
10611 err
10612 );
10613 }
10614 }
10615 });
10616 $(document).on("heartbeat-tick", (...args) => {
10617 const response = args[1];
10618 if (!response) {
10619 return;
10620 }
10621 for (const [field, set] of subscribers) {
10622 const value = response[field];
10623 if (value === void 0) {
10624 continue;
10625 }
10626 for (const cb of set) {
10627 try {
10628 cb(value);
10629 } catch (err) {
10630 console.error(
10631 `[desktop-mode/heartbeat] subscriber for "${field}" threw:`,
10632 err
10633 );
10634 }
10635 }
10636 }
10637 });
10638 }
10639 let _config = null;
10640 let _state = {
10641 installHintDismissed: false,
10642 notificationsEnabled: false
10643 };
10644 const _listeners = /* @__PURE__ */ new Set();
10645 function initPwaState(config) {
10646 if (!config) {
10647 _config = null;
10648 return;
10649 }
10650 _config = config;
10651 _state = { ...config.state };
10652 notify$e();
10653 }
10654 function getPwaState() {
10655 return { ..._state };
10656 }
10657 function updatePwaState(patch) {
10658 _state = { ..._state, ...patch };
10659 notify$e();
10660 if (!_config) {
10661 return getPwaState();
10662 }
10663 const body = JSON.stringify(patch);
10664 const nonce = readRestNonce$2();
10665 void fetch(_config.stateUrl, {
10666 method: "POST",
10667 credentials: "same-origin",
10668 headers: {
10669 "Content-Type": "application/json",
10670 ...nonce ? { "X-WP-Nonce": nonce } : {}
10671 },
10672 body
10673 }).catch((err) => {
10674 if (typeof console !== "undefined") {
10675 console.warn("[desktop-mode] pwa-state write failed:", err);
10676 }
10677 });
10678 return getPwaState();
10679 }
10680 function subscribePwaState(cb) {
10681 _listeners.add(cb);
10682 return () => {
10683 _listeners.delete(cb);
10684 };
10685 }
10686 function notify$e() {
10687 const snapshot = getPwaState();
10688 for (const cb of Array.from(_listeners)) {
10689 try {
10690 cb(snapshot);
10691 } catch (err) {
10692 if (typeof console !== "undefined") {
10693 console.error(
10694 "[desktop-mode] pwa-state listener threw:",
10695 err
10696 );
10697 }
10698 }
10699 }
10700 }
10701 function readRestNonce$2() {
10702 const cfg = window.desktopModeConfig;
10703 return cfg?.restNonce ?? "";
10704 }
10705 const state = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
10706 __proto__: null,
10707 getPwaState,
10708 initPwaState,
10709 subscribePwaState,
10710 updatePwaState
10711 }, Symbol.toStringTag, { value: "Module" }));
10712 function notify$d(options) {
10713 const intent = activity.filter(
10714 "desktop-mode/notification-requested",
10715 { ...options }
10716 );
10717 if (!intent || intent.cancel === true || !intent.title) {
10718 return () => void 0;
10719 }
10720 let dismissed = false;
10721 let dismissNative = null;
10722 let dismissToast = null;
10723 const dismiss = () => {
10724 if (dismissed) {
10725 return;
10726 }
10727 dismissed = true;
10728 if (dismissNative) {
10729 dismissNative();
10730 }
10731 if (dismissToast) {
10732 dismissToast();
10733 }
10734 };
10735 const fallback = () => {
10736 dismissToast = showToast({
10737 message: intent.body ? intent.title + " — " + intent.body : intent.title
10738 });
10739 activity.publish("desktop-mode/notification-shown", {
10740 ...intent,
10741 fallback: "toast"
10742 });
10743 };
10744 if (typeof window === "undefined" || typeof Notification === "undefined") {
10745 fallback();
10746 return dismiss;
10747 }
10748 const perm = Notification.permission;
10749 if (perm === "granted") {
10750 dismissNative = renderNative(intent);
10751 if (!dismissNative) {
10752 fallback();
10753 }
10754 return dismiss;
10755 }
10756 if (perm === "denied") {
10757 fallback();
10758 return dismiss;
10759 }
10760 void Notification.requestPermission().then((result) => {
10761 if (dismissed) {
10762 return;
10763 }
10764 if (result === "granted") {
10765 updatePwaState({ notificationsEnabled: true });
10766 dismissNative = renderNative(intent);
10767 if (!dismissNative) {
10768 fallback();
10769 }
10770 return;
10771 }
10772 fallback();
10773 });
10774 return dismiss;
10775 }
10776 function renderNative(intent) {
10777 let n = null;
10778 try {
10779 n = new Notification(intent.title, {
10780 body: intent.body,
10781 icon: intent.icon,
10782 tag: intent.tag,
10783 requireInteraction: intent.requireInteraction
10784 });
10785 } catch (err) {
10786 if (typeof console !== "undefined") {
10787 console.warn("[desktop-mode] Notification ctor threw:", err);
10788 }
10789 return null;
10790 }
10791 if (intent.onClick) {
10792 const handler = intent.onClick;
10793 n.onclick = () => {
10794 try {
10795 handler(n);
10796 } catch (hErr) {
10797 if (typeof console !== "undefined") {
10798 console.error(
10799 "[desktop-mode] notification onClick threw:",
10800 hErr
10801 );
10802 }
10803 }
10804 };
10805 }
10806 activity.publish("desktop-mode/notification-shown", {
10807 ...intent,
10808 fallback: null
10809 });
10810 return () => {
10811 if (n) {
10812 n.close();
10813 }
10814 };
10815 }
10816 async function requestNotificationPermission() {
10817 if (typeof Notification === "undefined") {
10818 return "unsupported";
10819 }
10820 if (Notification.permission !== "default") {
10821 return Notification.permission;
10822 }
10823 const result = await Notification.requestPermission();
10824 if (result === "granted") {
10825 updatePwaState({ notificationsEnabled: true });
10826 }
10827 return result;
10828 }
10829 function getNotificationPermission() {
10830 if (typeof Notification === "undefined") {
10831 return "unsupported";
10832 }
10833 return Notification.permission;
10834 }
10835 const store$d = createSharedStore(
10836 "desktop-mode/games-challenges",
10837 () => ({
10838 rows: /* @__PURE__ */ new Map(),
10839 version: 0,
10840 listeners: /* @__PURE__ */ new Set()
10841 })
10842 );
10843 function challengesState() {
10844 return store$d.state;
10845 }
10846 function ingestChallenges(rows) {
10847 const state2 = store$d.state;
10848 let changed = false;
10849 for (const row of rows) {
10850 if (!row || typeof row.id !== "number") {
10851 continue;
10852 }
10853 const prev = state2.rows.get(row.id);
10854 if (!prev || prev.updatedAtMs !== row.updatedAtMs) {
10855 state2.rows.set(row.id, row);
10856 changed = true;
10857 }
10858 if (row.updatedAtMs > state2.version) {
10859 state2.version = row.updatedAtMs;
10860 }
10861 }
10862 if (changed) {
10863 notify$c();
10864 }
10865 }
10866 function subscribeChallenges(cb) {
10867 store$d.state.listeners.add(cb);
10868 return () => {
10869 store$d.state.listeners.delete(cb);
10870 };
10871 }
10872 function notify$c() {
10873 for (const cb of Array.from(store$d.state.listeners)) {
10874 try {
10875 cb();
10876 } catch (err) {
10877 if (typeof console !== "undefined") {
10878 console.error(
10879 "[desktop-mode] challenges store listener threw:",
10880 err
10881 );
10882 }
10883 }
10884 }
10885 }
10886 function allChallenges() {
10887 return Array.from(store$d.state.rows.values()).sort(
10888 (a, b) => b.updatedAtMs - a.updatedAtMs
10889 );
10890 }
10891 const SOURCE = "desktop-mode/games";
10892 function restEnv() {
10893 const wpGlobal = window.wp;
10894 const config = wpGlobal?.desktop?.config;
10895 return {
10896 restUrl: config?.restUrl || "/wp-json/",
10897 restNonce: config?.restNonce || ""
10898 };
10899 }
10900 async function call$2(path, init2 = {}, opts = {}) {
10901 const { restUrl: restUrl2, restNonce } = restEnv();
10902 const headers = new Headers(init2.headers ?? {});
10903 headers.set("X-WP-Nonce", restNonce);
10904 if (init2.body && !headers.has("Content-Type")) {
10905 headers.set("Content-Type", "application/json");
10906 }
10907 const res = await trackedFetch$1(
10908 joinRestUrl(restUrl2, path),
10909 { ...init2, headers, credentials: "same-origin" },
10910 { source: SOURCE, windowId: opts.windowId, silent: opts.silent }
10911 );
10912 const body = await res.json().catch(() => null);
10913 if (!res.ok) {
10914 const message = body?.message || `Games request failed (${res.status})`;
10915 const error = new Error(message);
10916 error.status = res.status;
10917 throw error;
10918 }
10919 return body;
10920 }
10921 function submitScore(game, submission, opts = {}) {
10922 return call$2(
10923 `desktop-mode/v1/games/${game}/scores`,
10924 {
10925 method: "POST",
10926 body: JSON.stringify({
10927 score: submission.score,
10928 meta: submission.meta ?? {}
10929 })
10930 },
10931 opts
10932 );
10933 }
10934 function fetchPlaytime() {
10935 return call$2("desktop-mode/v1/games/playtime");
10936 }
10937 function recordPlaytime(game, seconds, opts = {}) {
10938 return call$2(
10939 `desktop-mode/v1/games/${game}/playtime`,
10940 {
10941 method: "POST",
10942 body: JSON.stringify({ seconds })
10943 },
10944 opts
10945 );
10946 }
10947 function acceptChallenge(id) {
10948 return call$2(`desktop-mode/v1/games/challenges/${id}/accept`, {
10949 method: "POST"
10950 });
10951 }
10952 function completeChallenge(id, submission, opts = {}) {
10953 return call$2(
10954 `desktop-mode/v1/games/challenges/${id}/complete`,
10955 {
10956 method: "POST",
10957 body: JSON.stringify({
10958 score: submission.score,
10959 meta: submission.meta ?? {}
10960 })
10961 },
10962 opts
10963 );
10964 }
10965 const FLUSH_INTERVAL_MS = 6e4;
10966 function startPlaytimeTracker(gameId, opts = {}) {
10967 let runningSince = Date.now();
10968 let bankedMs = 0;
10969 let stopped = false;
10970 const harvest = () => {
10971 if (runningSince === null) {
10972 return;
10973 }
10974 const now = Date.now();
10975 bankedMs += Math.max(0, now - runningSince);
10976 runningSince = now;
10977 };
10978 const flush = () => {
10979 harvest();
10980 const seconds = Math.floor(bankedMs / 1e3);
10981 if (seconds < 1) {
10982 return;
10983 }
10984 bankedMs -= seconds * 1e3;
10985 recordPlaytime(gameId, seconds, {
10986 windowId: opts.windowId,
10987 silent: true
10988 }).catch(() => {
10989 bankedMs += seconds * 1e3;
10990 });
10991 };
10992 const interval = setInterval(flush, FLUSH_INTERVAL_MS);
10993 return {
10994 pause: () => {
10995 harvest();
10996 runningSince = null;
10997 },
10998 resume: () => {
10999 if (stopped || runningSince !== null) {
11000 return;
11001 }
11002 runningSince = Date.now();
11003 },
11004 stop: () => {
11005 if (stopped) {
11006 return;
11007 }
11008 stopped = true;
11009 clearInterval(interval);
11010 harvest();
11011 runningSince = null;
11012 flush();
11013 }
11014 };
11015 }
11016 function desktopGlobal() {
11017 return window.wp?.desktop ?? {};
11018 }
11019 const DEFAULT_GAME_WIDTH = 760;
11020 const DEFAULT_GAME_HEIGHT = 560;
11021 const DEFAULT_GAME_MIN_WIDTH = 480;
11022 const DEFAULT_GAME_MIN_HEIGHT = 380;
11023 async function ensureGameRender(entry) {
11024 if (typeof entry.render === "function") {
11025 return entry;
11026 }
11027 const loadVendorScript2 = desktopGlobal().loadVendorScript;
11028 if (!entry.scriptUrl || typeof loadVendorScript2 !== "function") {
11029 throw new Error(
11030 `[desktop-mode] Game "${entry.id}" has no render callback and no loadable script.`
11031 );
11032 }
11033 await loadVendorScript2(entry.scriptUrl, {
11034 translations: entry.scriptTranslations,
11035 l10n: entry.scriptL10n,
11036 before: entry.scriptBefore,
11037 after: entry.scriptAfter
11038 });
11039 const globals = window;
11040 const def = globals.desktopModeGames?.[entry.id];
11041 if (!def || typeof def.render !== "function") {
11042 throw new Error(
11043 `[desktop-mode] No game def on window.desktopModeGames["${entry.id}"]. Script loaded but didn't publish a def — check the plugin's global assignment.`
11044 );
11045 }
11046 const upgraded = {
11047 ...entry,
11048 render: def.render,
11049 window: def.window ?? entry.window
11050 };
11051 register$1(upgraded);
11052 return upgraded;
11053 }
11054 async function launchGame(id, opts = {}) {
11055 const desktop = desktopGlobal();
11056 let entry = get$1(id);
11057 if (!entry) {
11058 throw new Error(`[desktop-mode] Unknown game "${id}".`);
11059 }
11060 entry = await ensureGameRender(entry);
11061 const render2 = entry.render;
11062 if (typeof render2 !== "function") {
11063 throw new Error(
11064 `[desktop-mode] Game "${id}" did not provide a render callback.`
11065 );
11066 }
11067 if (typeof desktop.registerWindow !== "function") {
11068 throw new Error(
11069 "[desktop-mode] wp.desktop.registerWindow is missing — the shell must boot before launching games."
11070 );
11071 }
11072 const windowId = `desktop-mode-game-${id}`;
11073 const suspendReason = `game:${windowId}`;
11074 const manager2 = desktop.windowManager;
11075 const existing = manager2?.getByBaseId?.(windowId) ?? manager2?.getById(windowId);
11076 if (existing) {
11077 const winDesktop = existing.config?.desktopId;
11078 if (winDesktop && manager2?.switchDesktop && winDesktop !== manager2?.getActiveDesktopId?.()) {
11079 manager2.switchDesktop(winDesktop);
11080 }
11081 void desktop.registerWindow({
11082 id: windowId,
11083 title: entry.title,
11084 icon: entry.icon,
11085 render: () => void 0
11086 });
11087 return;
11088 }
11089 desktop.wallpaper?.suspend(suspendReason);
11090 let resumed = false;
11091 const resumeOnce = () => {
11092 if (resumed) {
11093 return;
11094 }
11095 resumed = true;
11096 desktop.wallpaper?.resume(suspendReason);
11097 };
11098 let tracker = null;
11099 const stopTracker = () => {
11100 tracker?.stop();
11101 tracker = null;
11102 };
11103 desktop.onWindow?.(windowId, {
11104 closed: () => {
11105 stopTracker();
11106 resumeOnce();
11107 },
11108 minimized: () => tracker?.pause(),
11109 restored: () => tracker?.resume()
11110 });
11111 const submit = (result) => {
11112 if (opts.challenge) {
11113 return completeChallenge(opts.challenge.id, result, {
11114 windowId
11115 }).then(() => void 0);
11116 }
11117 return submitScore(id, result, { windowId }).then(
11118 () => void 0
11119 );
11120 };
11121 try {
11122 await desktop.registerWindow({
11123 id: windowId,
11124 title: entry.title,
11125 icon: entry.icon,
11126 width: entry.window?.width ?? DEFAULT_GAME_WIDTH,
11127 height: entry.window?.height ?? DEFAULT_GAME_HEIGHT,
11128 minWidth: entry.window?.minWidth ?? DEFAULT_GAME_MIN_WIDTH,
11129 minHeight: entry.window?.minHeight ?? DEFAULT_GAME_MIN_HEIGHT,
11130 render: (body) => {
11131 const ctx = {
11132 windowId,
11133 container: body,
11134 config: entry.config ?? {},
11135 challenge: opts.challenge,
11136 submitScore: submit,
11137 close: () => {
11138 desktop.windowManager?.getById(windowId)?.close();
11139 }
11140 };
11141 tracker = startPlaytimeTracker(id, { windowId });
11142 const teardown = render2(ctx);
11143 return () => {
11144 try {
11145 teardown?.();
11146 } finally {
11147 stopTracker();
11148 resumeOnce();
11149 }
11150 };
11151 }
11152 });
11153 } catch (err) {
11154 stopTracker();
11155 resumeOnce();
11156 throw err;
11157 }
11158 }
11159 function gameTitle(id) {
11160 return get$1(id)?.title || id;
11161 }
11162 const promptedPending = /* @__PURE__ */ new Set();
11163 const promptedCompleted = /* @__PURE__ */ new Set();
11164 async function acceptAndPlay(row) {
11165 const { challenge } = await acceptChallenge(row.id);
11166 ingestChallenges([challenge]);
11167 await launchGame(row.game, {
11168 challenge: {
11169 id: row.id,
11170 scoreToBeat: row.scoreToBeat,
11171 scoreMeta: row.scoreMeta,
11172 challengerName: row.challengerName
11173 }
11174 });
11175 }
11176 function promptRecipient(row) {
11177 const message = sprintf(
11178 /* translators: 1: challenger display name, 2: game title/slug, 3: score. */
11179 __("%1$s challenged you to %2$s — beat %3$s!"),
11180 row.challengerName,
11181 gameTitle(row.game),
11182 String(row.scoreToBeat)
11183 );
11184 notify$d({
11185 title: __("Game challenge"),
11186 body: message,
11187 tag: `desktop-mode-game-challenge-${row.id}`
11188 });
11189 showToast({
11190 message,
11191 persistent: true,
11192 dismissible: true,
11193 action: {
11194 label: __("Accept & Play"),
11195 onClick: () => {
11196 void acceptAndPlay(row).catch((err) => {
11197 showToast({
11198 message: err instanceof Error ? err.message : __("Could not accept the challenge.")
11199 });
11200 });
11201 }
11202 }
11203 });
11204 }
11205 function promptChallenger(row) {
11206 let format;
11207 if ("beaten" === row.result) {
11208 format = __("%1$s beat your score: %2$s vs your %3$s.");
11209 } else {
11210 format = __("%1$s did not beat your score: %2$s vs your %3$s.");
11211 }
11212 const message = sprintf(
11213 format,
11214 row.recipientName,
11215 String(row.resultScore ?? 0),
11216 String(row.scoreToBeat)
11217 );
11218 notify$d({
11219 title: __("Challenge finished"),
11220 body: message,
11221 tag: `desktop-mode-game-challenge-${row.id}`
11222 });
11223 showToast({ message });
11224 }
11225 function bootGamesChallenges(deps2) {
11226 const { currentUserId } = deps2;
11227 if (!currentUserId) {
11228 return;
11229 }
11230 heartbeat.contribute("desktop_mode_games_subscribe", () => ({
11231 challengesVersion: challengesState().version
11232 }));
11233 heartbeat.subscribe("desktop_mode_games", (payload) => {
11234 if (Array.isArray(payload?.challenges)) {
11235 ingestChallenges(payload.challenges);
11236 }
11237 });
11238 const scan = () => {
11239 for (const row of allChallenges()) {
11240 if ("pending" === row.state && row.recipientId === currentUserId && !promptedPending.has(row.id)) {
11241 promptedPending.add(row.id);
11242 promptRecipient(row);
11243 }
11244 if ("completed" === row.state && row.challengerId === currentUserId && !promptedCompleted.has(row.id)) {
11245 promptedCompleted.add(row.id);
11246 promptChallenger(row);
11247 }
11248 }
11249 };
11250 subscribeChallenges(scan);
11251 scan();
11252 }
11253 const COMMAND_SLUG = /^[a-z0-9_/-]+$/;
11254 const commandRegistryStore = createSharedStore(
11255 "desktop-mode/commands-registry",
11256 () => ({
11257 registry: /* @__PURE__ */ new Map(),
11258 listeners: /* @__PURE__ */ new Set()
11259 })
11260 );
11261 const registry$8 = commandRegistryStore.state.registry;
11262 const listeners$b = commandRegistryStore.state.listeners;
11263 function registerCommand(cmd) {
11264 const errors = [];
11265 const slug = typeof cmd?.slug === "string" ? cmd.slug.trim().toLowerCase() : "";
11266 if (!cmd || typeof cmd !== "object") {
11267 errors.push("def (not an object)");
11268 } else {
11269 if (typeof cmd.slug !== "string" || cmd.slug.trim() === "") {
11270 errors.push("slug (missing)");
11271 } else if (!COMMAND_SLUG.test(slug)) {
11272 errors.push(
11273 `slug (must match ${COMMAND_SLUG} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
11274 );
11275 }
11276 if (typeof cmd.label !== "string" || cmd.label.trim() === "") {
11277 errors.push("label (missing)");
11278 }
11279 if (typeof cmd.run !== "function") {
11280 errors.push("run (must be a function)");
11281 }
11282 }
11283 throwOnRegistrationErrors("Command", errors, cmd);
11284 registry$8.set(slug, { ...cmd, slug });
11285 notify$b();
11286 }
11287 function unregisterCommand(slug) {
11288 if (registry$8.delete(slug.toLowerCase())) {
11289 notify$b();
11290 }
11291 }
11292 function unregisterByOwner(owner) {
11293 if (!owner) {
11294 return 0;
11295 }
11296 let removed = 0;
11297 for (const [slug, cmd] of Array.from(registry$8.entries())) {
11298 if (cmd.owner === owner) {
11299 registry$8.delete(slug);
11300 removed++;
11301 }
11302 }
11303 if (removed > 0) {
11304 notify$b();
11305 }
11306 return removed;
11307 }
11308 function listCommands() {
11309 return Array.from(registry$8.values());
11310 }
11311 function listAiCallableCommands() {
11312 const out = [];
11313 for (const cmd of registry$8.values()) {
11314 if (cmd.aiCallable !== true) {
11315 continue;
11316 }
11317 out.push({
11318 slug: cmd.slug,
11319 label: cmd.label,
11320 description: cmd.description ?? "",
11321 hint: cmd.hint ?? ""
11322 });
11323 }
11324 return out;
11325 }
11326 function findCommand(slug) {
11327 return registry$8.get(slug.toLowerCase()) ?? null;
11328 }
11329 function notify$b() {
11330 const snapshot = Array.from(listeners$b);
11331 for (const cb of snapshot) {
11332 try {
11333 cb();
11334 } catch (err) {
11335 if (typeof console !== "undefined") {
11336 console.error("[desktop-mode] command-registry listener threw:", err);
11337 }
11338 }
11339 }
11340 }
11341 function createCommandRegistrySync() {
11342 const loadedHandles = /* @__PURE__ */ new Set();
11343 const loadedUrls = /* @__PURE__ */ new Set();
11344 let prevSlugsByHandle = /* @__PURE__ */ new Map();
11345 const ensureScript = async (entry) => {
11346 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
11347 loadedHandles.add(entry.handle);
11348 return;
11349 }
11350 try {
11351 await loadVendorScript(entry.scriptUrl, {
11352 translations: entry.scriptTranslations,
11353 l10n: entry.scriptL10n,
11354 before: entry.scriptBefore,
11355 after: entry.scriptAfter
11356 });
11357 } catch (err) {
11358 doAction(HOOKS.SHELL_ERROR, {
11359 scope: "command-script-load",
11360 handle: entry.handle,
11361 url: entry.scriptUrl,
11362 error: err
11363 });
11364 return;
11365 }
11366 loadedUrls.add(entry.scriptUrl);
11367 loadedHandles.add(entry.handle);
11368 };
11369 const slugsByHandleFrom = (commands) => {
11370 const map = /* @__PURE__ */ new Map();
11371 if (!commands) {
11372 return map;
11373 }
11374 for (const entry of commands) {
11375 if (!entry.scriptHandle || !entry.slug) {
11376 continue;
11377 }
11378 let set = map.get(entry.scriptHandle);
11379 if (!set) {
11380 set = /* @__PURE__ */ new Set();
11381 map.set(entry.scriptHandle, set);
11382 }
11383 set.add(entry.slug);
11384 }
11385 return map;
11386 };
11387 const collectSlugsToRemove = (handle) => {
11388 const slugs = /* @__PURE__ */ new Set();
11389 for (const cmd of listCommands()) {
11390 if (cmd.owner === handle) {
11391 slugs.add(cmd.slug);
11392 }
11393 }
11394 const declared = prevSlugsByHandle.get(handle);
11395 if (declared) {
11396 for (const slug of declared) {
11397 slugs.add(slug);
11398 }
11399 }
11400 return slugs;
11401 };
11402 return async (scripts, commands) => {
11403 const incomingHandles = /* @__PURE__ */ new Set();
11404 for (const entry of scripts) {
11405 if (entry.handle) {
11406 incomingHandles.add(entry.handle);
11407 }
11408 }
11409 for (const handle of Array.from(loadedHandles)) {
11410 if (incomingHandles.has(handle)) {
11411 continue;
11412 }
11413 for (const slug of collectSlugsToRemove(handle)) {
11414 unregisterCommand(slug);
11415 }
11416 loadedHandles.delete(handle);
11417 }
11418 for (const entry of scripts) {
11419 if (!entry.handle || loadedHandles.has(entry.handle)) {
11420 continue;
11421 }
11422 await ensureScript(entry);
11423 }
11424 prevSlugsByHandle = slugsByHandleFrom(commands);
11425 };
11426 }
11427 const store$c = createSharedStore(
11428 "desktop-mode/settings-tab-registry",
11429 () => ({
11430 registry: /* @__PURE__ */ new Map(),
11431 listeners: /* @__PURE__ */ new Set()
11432 })
11433 );
11434 const registry$7 = store$c.state.registry;
11435 const listeners$a = store$c.state.listeners;
11436 function registerSettingsTab(tab) {
11437 if (!tab || typeof tab.id !== "string" || tab.id.trim() === "") {
11438 return;
11439 }
11440 if (typeof tab.label !== "string" || tab.label.trim() === "") {
11441 return;
11442 }
11443 if (typeof tab.render !== "function") {
11444 return;
11445 }
11446 const id = tab.id.trim().toLowerCase();
11447 if (!/^[a-z0-9_\-]+$/.test(id)) {
11448 if (typeof console !== "undefined") {
11449 console.warn(
11450 "[desktop-mode] registerSettingsTab: id must be [a-z0-9_-]+, got",
11451 tab.id
11452 );
11453 }
11454 return;
11455 }
11456 registry$7.set(id, { ...tab, id });
11457 notify$a();
11458 }
11459 function unregisterSettingsTab(id) {
11460 if (registry$7.delete(id.toLowerCase())) {
11461 notify$a();
11462 }
11463 }
11464 function unregisterSettingsTabsByOwner(owner) {
11465 if (!owner) {
11466 return 0;
11467 }
11468 let removed = 0;
11469 for (const [id, tab] of Array.from(registry$7.entries())) {
11470 if (tab.owner === owner) {
11471 registry$7.delete(id);
11472 removed++;
11473 }
11474 }
11475 if (removed > 0) {
11476 notify$a();
11477 }
11478 return removed;
11479 }
11480 function listSettingsTabs() {
11481 return Array.from(registry$7.values()).sort(
11482 (a, b) => (a.order ?? 100) - (b.order ?? 100)
11483 );
11484 }
11485 function notify$a() {
11486 const snapshot = Array.from(listeners$a);
11487 for (const cb of snapshot) {
11488 try {
11489 cb();
11490 } catch (err) {
11491 if (typeof console !== "undefined") {
11492 console.error(
11493 "[desktop-mode] settings-tab-registry listener threw:",
11494 err
11495 );
11496 }
11497 }
11498 }
11499 }
11500 function createSettingsTabRegistrySync() {
11501 const loadedHandles = /* @__PURE__ */ new Set();
11502 const loadedUrls = /* @__PURE__ */ new Set();
11503 let prevIdsByHandle = /* @__PURE__ */ new Map();
11504 const ensureScript = async (entry) => {
11505 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
11506 loadedHandles.add(entry.handle);
11507 return;
11508 }
11509 try {
11510 await loadVendorScript(entry.scriptUrl, {
11511 translations: entry.scriptTranslations,
11512 l10n: entry.scriptL10n,
11513 before: entry.scriptBefore,
11514 after: entry.scriptAfter
11515 });
11516 } catch (err) {
11517 doAction(HOOKS.SHELL_ERROR, {
11518 scope: "settings-tab-script-load",
11519 handle: entry.handle,
11520 url: entry.scriptUrl,
11521 error: err
11522 });
11523 return;
11524 }
11525 loadedUrls.add(entry.scriptUrl);
11526 loadedHandles.add(entry.handle);
11527 };
11528 const idsByHandleFrom = (tabs) => {
11529 const map = /* @__PURE__ */ new Map();
11530 if (!tabs) {
11531 return map;
11532 }
11533 for (const entry of tabs) {
11534 if (!entry.scriptHandle || !entry.id) {
11535 continue;
11536 }
11537 let set = map.get(entry.scriptHandle);
11538 if (!set) {
11539 set = /* @__PURE__ */ new Set();
11540 map.set(entry.scriptHandle, set);
11541 }
11542 set.add(entry.id);
11543 }
11544 return map;
11545 };
11546 const removeByHandle = (handle) => {
11547 unregisterSettingsTabsByOwner(handle);
11548 const declared = prevIdsByHandle.get(handle);
11549 if (declared) {
11550 const present = new Set(
11551 listSettingsTabs().map((t) => t.id)
11552 );
11553 for (const id of declared) {
11554 if (present.has(id)) {
11555 unregisterSettingsTab(id);
11556 }
11557 }
11558 }
11559 };
11560 return async (scripts, tabs) => {
11561 const incomingHandles = /* @__PURE__ */ new Set();
11562 for (const entry of scripts) {
11563 if (entry.handle) {
11564 incomingHandles.add(entry.handle);
11565 }
11566 }
11567 for (const handle of Array.from(loadedHandles)) {
11568 if (incomingHandles.has(handle)) {
11569 continue;
11570 }
11571 removeByHandle(handle);
11572 loadedHandles.delete(handle);
11573 }
11574 for (const entry of scripts) {
11575 if (!entry.handle || loadedHandles.has(entry.handle)) {
11576 continue;
11577 }
11578 await ensureScript(entry);
11579 }
11580 prevIdsByHandle = idsByHandleFrom(tabs);
11581 };
11582 }
11583 const store$b = createSharedStore(
11584 "desktop-mode/title-bar-buttons-registry",
11585 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
11586 );
11587 const registry$6 = store$b.state.registry;
11588 const listeners$9 = store$b.state.listeners;
11589 const TITLE_BAR_BUTTON_ID = /^[a-z0-9_/-]+$/;
11590 function registerTitleBarButton(def) {
11591 const errors = [];
11592 if (!def || typeof def !== "object") {
11593 errors.push("def (not an object)");
11594 } else {
11595 if (typeof def.id !== "string" || def.id.trim() === "") {
11596 errors.push("id (missing)");
11597 } else if (!TITLE_BAR_BUTTON_ID.test(def.id.trim().toLowerCase())) {
11598 errors.push(
11599 `id (must match ${TITLE_BAR_BUTTON_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
11600 );
11601 }
11602 if (typeof def.label !== "string" || def.label.trim() === "") {
11603 errors.push("label (missing)");
11604 }
11605 if (typeof def.icon !== "string" || def.icon.trim() === "") {
11606 errors.push("icon (missing)");
11607 }
11608 if (typeof def.match !== "function") {
11609 errors.push("match (must be a function)");
11610 }
11611 if (typeof def.onClick !== "function" && typeof def.render !== "function") {
11612 errors.push("onClick|render (at least one must be a function)");
11613 }
11614 }
11615 throwOnRegistrationErrors("TitleBarButton", errors, def);
11616 const id = def.id.trim().toLowerCase();
11617 registry$6.set(id, { ...def, id });
11618 notify$9();
11619 }
11620 function unregisterTitleBarButton(id) {
11621 if (registry$6.delete(id.toLowerCase())) {
11622 notify$9();
11623 }
11624 }
11625 function unregisterTitleBarButtonsByOwner(owner) {
11626 if (!owner) {
11627 return 0;
11628 }
11629 let removed = 0;
11630 for (const [id, def] of Array.from(registry$6.entries())) {
11631 if (def.owner === owner) {
11632 registry$6.delete(id);
11633 removed++;
11634 }
11635 }
11636 if (removed > 0) {
11637 notify$9();
11638 }
11639 return removed;
11640 }
11641 function listTitleBarButtons() {
11642 return Array.from(registry$6.values()).sort(
11643 (a, b) => (a.order ?? 100) - (b.order ?? 100)
11644 );
11645 }
11646 function notify$9() {
11647 const snapshot = Array.from(listeners$9);
11648 for (const cb of snapshot) {
11649 try {
11650 cb();
11651 } catch (err) {
11652 if (typeof console !== "undefined") {
11653 console.error(
11654 "[desktop-mode] title-bar-button registry listener threw:",
11655 err
11656 );
11657 }
11658 }
11659 }
11660 }
11661 function createTitleBarButtonRegistrySync() {
11662 const loadedHandles = /* @__PURE__ */ new Set();
11663 const loadedUrls = /* @__PURE__ */ new Set();
11664 const ensureScript = async (entry) => {
11665 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
11666 loadedHandles.add(entry.handle);
11667 return;
11668 }
11669 try {
11670 await loadVendorScript(entry.scriptUrl, {
11671 translations: entry.scriptTranslations,
11672 l10n: entry.scriptL10n,
11673 before: entry.scriptBefore,
11674 after: entry.scriptAfter
11675 });
11676 } catch (err) {
11677 doAction(HOOKS.SHELL_ERROR, {
11678 scope: "titlebar-button-script-load",
11679 handle: entry.handle,
11680 url: entry.scriptUrl,
11681 error: err
11682 });
11683 return;
11684 }
11685 loadedUrls.add(entry.scriptUrl);
11686 loadedHandles.add(entry.handle);
11687 };
11688 return async (scripts) => {
11689 const incomingHandles = /* @__PURE__ */ new Set();
11690 for (const entry of scripts) {
11691 if (entry.handle) {
11692 incomingHandles.add(entry.handle);
11693 }
11694 }
11695 for (const handle of Array.from(loadedHandles)) {
11696 if (incomingHandles.has(handle)) {
11697 continue;
11698 }
11699 unregisterTitleBarButtonsByOwner(handle);
11700 loadedHandles.delete(handle);
11701 }
11702 for (const entry of scripts) {
11703 if (!entry.handle || loadedHandles.has(entry.handle)) {
11704 continue;
11705 }
11706 await ensureScript(entry);
11707 }
11708 };
11709 }
11710 const WINDOW_LINK_RENDERER_NONE = "none";
11711 const WINDOW_LINK_RENDERER_DEFAULT = "svg-splines";
11712 const store$a = createSharedStore(
11713 "desktop-mode/window-link-renderer-registry",
11714 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
11715 );
11716 const registry$5 = store$a.state.registry;
11717 const listeners$8 = store$a.state.listeners;
11718 const WINDOW_LINK_RENDERER_ID = /^[a-z0-9_/-]+$/;
11719 function registerWindowLinkRenderer(def) {
11720 const errors = [];
11721 if (!def || typeof def !== "object") {
11722 errors.push("def (not an object)");
11723 } else {
11724 if (typeof def.id !== "string" || def.id.trim() === "") {
11725 errors.push("id (missing)");
11726 } else if (!WINDOW_LINK_RENDERER_ID.test(def.id.trim().toLowerCase())) {
11727 errors.push(
11728 `id (must match ${WINDOW_LINK_RENDERER_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
11729 );
11730 } else if (def.id.trim().toLowerCase() === WINDOW_LINK_RENDERER_NONE) {
11731 errors.push('id ("none" is reserved)');
11732 }
11733 if (typeof def.label !== "string" || def.label.trim() === "") {
11734 errors.push("label (missing)");
11735 }
11736 if (typeof def.mount !== "function") {
11737 errors.push("mount (not a function)");
11738 }
11739 }
11740 throwOnRegistrationErrors("WindowLinkRenderer", errors, def);
11741 const id = def.id.trim().toLowerCase();
11742 registry$5.set(id, { ...def, id });
11743 notify$8();
11744 }
11745 function unregisterWindowLinkRenderer(id) {
11746 if (registry$5.delete(id.toLowerCase())) {
11747 notify$8();
11748 }
11749 }
11750 function unregisterWindowLinkRenderersByOwner(owner) {
11751 if (!owner) {
11752 return 0;
11753 }
11754 let removed = 0;
11755 for (const [id, def] of Array.from(registry$5.entries())) {
11756 if (def.owner === owner) {
11757 registry$5.delete(id);
11758 removed++;
11759 }
11760 }
11761 if (removed > 0) {
11762 notify$8();
11763 }
11764 return removed;
11765 }
11766 function listWindowLinkRenderers() {
11767 const copy = Array.from(registry$5.values());
11768 const filtered = applyFilters(
11769 HOOKS.WINDOW_LINK_RENDERERS,
11770 copy
11771 );
11772 if (!Array.isArray(filtered)) {
11773 if (typeof console !== "undefined") {
11774 console.warn(
11775 "[desktop-mode] `desktop-mode.window-links.renderers` filter returned a non-array; falling back to registry list."
11776 );
11777 }
11778 return copy;
11779 }
11780 return filtered;
11781 }
11782 function getWindowLinkRenderer(id) {
11783 return listWindowLinkRenderers().find((r) => r.id === id);
11784 }
11785 function subscribeWindowLinkRenderers(cb) {
11786 listeners$8.add(cb);
11787 return () => {
11788 listeners$8.delete(cb);
11789 };
11790 }
11791 function notify$8() {
11792 const snapshot = Array.from(listeners$8);
11793 for (const cb of snapshot) {
11794 try {
11795 cb();
11796 } catch (err) {
11797 if (typeof console !== "undefined") {
11798 console.error(
11799 "[desktop-mode] window-link-renderer registry listener threw:",
11800 err
11801 );
11802 }
11803 }
11804 }
11805 }
11806 const MIN_VISIBLE_SEGMENT = 16;
11807 function subtractIntervals(base, holes) {
11808 const sorted = holes.map((h) => ({
11809 start: Math.max(base.start, h.start),
11810 end: Math.min(base.end, h.end)
11811 })).filter((h) => h.end > h.start).sort((a, b) => a.start - b.start);
11812 const out = [];
11813 let cursor = base.start;
11814 for (const hole of sorted) {
11815 if (hole.start > cursor) {
11816 out.push({ start: cursor, end: hole.start });
11817 }
11818 cursor = Math.max(cursor, hole.end);
11819 }
11820 if (cursor < base.end) {
11821 out.push({ start: cursor, end: base.end });
11822 }
11823 return out;
11824 }
11825 function anchorOnBorder(rect, toward) {
11826 const cx = rect.x + rect.width / 2;
11827 const cy = rect.y + rect.height / 2;
11828 const dx = toward.x - cx;
11829 const dy = toward.y - cy;
11830 if (dx === 0 && dy === 0) {
11831 return { x: cx, y: cy, side: "right" };
11832 }
11833 const sx = dx !== 0 ? rect.width / 2 / Math.abs(dx) : Infinity;
11834 const sy = dy !== 0 ? rect.height / 2 / Math.abs(dy) : Infinity;
11835 const s = Math.min(sx, sy);
11836 const x = cx + dx * s;
11837 const y = cy + dy * s;
11838 let side;
11839 if (sx <= sy) {
11840 side = dx > 0 ? "right" : "left";
11841 } else {
11842 side = dy > 0 ? "bottom" : "top";
11843 }
11844 return { x, y, side };
11845 }
11846 function isPointVisible(point, zIndex, obstacles, selfId) {
11847 for (const o of obstacles) {
11848 if (o.windowId === selfId || o.zIndex <= zIndex) {
11849 continue;
11850 }
11851 if (point.x >= o.rect.x && point.x <= o.rect.x + o.rect.width && point.y >= o.rect.y && point.y <= o.rect.y + o.rect.height) {
11852 return false;
11853 }
11854 }
11855 return true;
11856 }
11857 function visibleBorderAnchor(rect, zIndex, obstacles, selfId, toward) {
11858 const occluders = obstacles.filter(
11859 (o) => o.windowId !== selfId && o.zIndex > zIndex
11860 );
11861 const sides = [
11862 {
11863 side: "top",
11864 base: { start: rect.x, end: rect.x + rect.width },
11865 at: rect.y,
11866 horizontal: true
11867 },
11868 {
11869 side: "bottom",
11870 base: { start: rect.x, end: rect.x + rect.width },
11871 at: rect.y + rect.height,
11872 horizontal: true
11873 },
11874 {
11875 side: "left",
11876 base: { start: rect.y, end: rect.y + rect.height },
11877 at: rect.x,
11878 horizontal: false
11879 },
11880 {
11881 side: "right",
11882 base: { start: rect.y, end: rect.y + rect.height },
11883 at: rect.x + rect.width,
11884 horizontal: false
11885 }
11886 ];
11887 let best = null;
11888 let bestDistance = Infinity;
11889 for (const { side, base, at, horizontal } of sides) {
11890 const holes = [];
11891 for (const { rect: o } of occluders) {
11892 const coversLine = horizontal ? o.y <= at && at <= o.y + o.height : o.x <= at && at <= o.x + o.width;
11893 if (!coversLine) {
11894 continue;
11895 }
11896 holes.push(
11897 horizontal ? { start: o.x, end: o.x + o.width } : { start: o.y, end: o.y + o.height }
11898 );
11899 }
11900 for (const segment of subtractIntervals(base, holes)) {
11901 if (segment.end - segment.start < MIN_VISIBLE_SEGMENT) {
11902 continue;
11903 }
11904 const mid = (segment.start + segment.end) / 2;
11905 const x = horizontal ? mid : at;
11906 const y = horizontal ? at : mid;
11907 const distance2 = Math.hypot(toward.x - x, toward.y - y);
11908 if (distance2 < bestDistance) {
11909 bestDistance = distance2;
11910 best = { x, y, side };
11911 }
11912 }
11913 }
11914 return best;
11915 }
11916 function closestBorderAnchors(a, b) {
11917 const gapX = Math.max(b.x - (a.x + a.width), a.x - (b.x + b.width));
11918 const gapY = Math.max(
11919 b.y - (a.y + a.height),
11920 a.y - (b.y + b.height)
11921 );
11922 if (gapX < 0 && gapY < 0) {
11923 return null;
11924 }
11925 const overlapX1 = Math.max(a.x, b.x);
11926 const overlapX2 = Math.min(a.x + a.width, b.x + b.width);
11927 const overlapY1 = Math.max(a.y, b.y);
11928 const overlapY2 = Math.min(a.y + a.height, b.y + b.height);
11929 let ax;
11930 let bx;
11931 if (overlapX2 >= overlapX1) {
11932 ax = bx = (overlapX1 + overlapX2) / 2;
11933 } else if (b.x > a.x) {
11934 ax = a.x + a.width;
11935 bx = b.x;
11936 } else {
11937 ax = a.x;
11938 bx = b.x + b.width;
11939 }
11940 let ay;
11941 let by;
11942 if (overlapY2 >= overlapY1) {
11943 ay = by = (overlapY1 + overlapY2) / 2;
11944 } else if (b.y > a.y) {
11945 ay = a.y + a.height;
11946 by = b.y;
11947 } else {
11948 ay = a.y;
11949 by = b.y + b.height;
11950 }
11951 const horizontal = gapX >= gapY;
11952 const sideOf = (rect, x, y) => {
11953 if (horizontal) {
11954 return x <= rect.x ? "left" : "right";
11955 }
11956 return y <= rect.y ? "top" : "bottom";
11957 };
11958 return {
11959 from: { x: ax, y: ay, side: sideOf(a, ax, ay) },
11960 to: { x: bx, y: by, side: sideOf(b, bx, by) }
11961 };
11962 }
11963 function controlPoint(anchor, distance2) {
11964 const k = Math.min(160, Math.max(24, 0.4 * distance2));
11965 switch (anchor.side) {
11966 case "left":
11967 return { x: anchor.x - k, y: anchor.y };
11968 case "right":
11969 return { x: anchor.x + k, y: anchor.y };
11970 case "top":
11971 return { x: anchor.x, y: anchor.y - k };
11972 default:
11973 return { x: anchor.x, y: anchor.y + k };
11974 }
11975 }
11976 function centerOf(rect) {
11977 return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
11978 }
11979 const SVG_NS = "http://www.w3.org/2000/svg";
11980 let _mountSeq = 0;
11981 function endpointAnchor(rect, zIndex, windowId, obstacles, toward) {
11982 const classic = anchorOnBorder(rect, toward);
11983 if (zIndex === null || isPointVisible(classic, zIndex, obstacles, windowId)) {
11984 return classic;
11985 }
11986 return visibleBorderAnchor(rect, zIndex, obstacles, windowId, toward) ?? classic;
11987 }
11988 function buildMarkers(svg, idBase) {
11989 const defs = document.createElementNS(SVG_NS, "defs");
11990 const make = (suffix, className, size) => {
11991 const id = `${idBase}-${suffix}`;
11992 const marker = document.createElementNS(SVG_NS, "marker");
11993 marker.setAttribute("id", id);
11994 marker.setAttribute("viewBox", "0 0 10 10");
11995 marker.setAttribute("refX", "5");
11996 marker.setAttribute("refY", "5");
11997 marker.setAttribute("markerWidth", size);
11998 marker.setAttribute("markerHeight", size);
11999 marker.setAttribute("markerUnits", "strokeWidth");
12000 const tip = document.createElementNS(SVG_NS, "circle");
12001 tip.setAttribute("cx", "5");
12002 tip.setAttribute("cy", "5");
12003 tip.setAttribute("r", "4");
12004 tip.classList.add(className);
12005 marker.appendChild(tip);
12006 defs.appendChild(marker);
12007 return id;
12008 };
12009 const endpoint = "desktop-mode-window-link__endpoint";
12010 const active2 = `${endpoint}--active`;
12011 const markers = {
12012 dot: {
12013 normal: make("dot", endpoint, "7"),
12014 active: make("dot-active", active2, "7")
12015 },
12016 port: {
12017 normal: make("port", endpoint, "4.5"),
12018 active: make("port-active", active2, "4.5")
12019 }
12020 };
12021 svg.appendChild(defs);
12022 return markers;
12023 }
12024 registerWindowLinkRenderer({
12025 id: "svg-splines",
12026 label: __("Splines"),
12027 description: __(
12028 "Curved connectors between related windows, ending in circular dots — the larger dot sits on the window the content belongs to; windows that reference each other get large dots on both ends."
12029 ),
12030 mount: (ctx) => {
12031 const seq = ++_mountSeq;
12032 const buildSurface = (container, suffix) => {
12033 const svg = document.createElementNS(SVG_NS, "svg");
12034 svg.classList.add("desktop-mode-window-links__svg");
12035 container.appendChild(svg);
12036 return {
12037 svg,
12038 markers: buildMarkers(
12039 svg,
12040 `desktop-mode-window-link-${seq}${suffix}`
12041 )
12042 };
12043 };
12044 const surfaces = {
12045 base: buildSurface(ctx.container, ""),
12046 elevated: buildSurface(ctx.elevatedContainer, "-elevated")
12047 };
12048 const edges = /* @__PURE__ */ new Map();
12049 const draw = (frame) => {
12050 for (const { svg } of [surfaces.base, surfaces.elevated]) {
12051 svg.setAttribute("width", String(frame.container.width));
12052 svg.setAttribute(
12053 "height",
12054 String(frame.container.height)
12055 );
12056 svg.setAttribute(
12057 "viewBox",
12058 `0 0 ${frame.container.width} ${frame.container.height}`
12059 );
12060 }
12061 const seen = /* @__PURE__ */ new Set();
12062 for (const edge of frame.edges) {
12063 if (!edge.from || !edge.to) {
12064 continue;
12065 }
12066 const key = `${edge.fromWindowId}→${edge.toWindowId}:${edge.kind}`;
12067 seen.add(key);
12068 const surfaceName = edge.elevated ? "elevated" : "base";
12069 let el = edges.get(key);
12070 if (el && el.surface !== surfaceName) {
12071 el.group.remove();
12072 edges.delete(key);
12073 el = void 0;
12074 }
12075 if (!el) {
12076 const group = document.createElementNS(SVG_NS, "g");
12077 group.classList.add("desktop-mode-window-link");
12078 const path = document.createElementNS(SVG_NS, "path");
12079 path.classList.add("desktop-mode-window-link__path");
12080 group.appendChild(path);
12081 surfaces[surfaceName].svg.appendChild(group);
12082 el = { group, path, surface: surfaceName };
12083 edges.set(key, el);
12084 }
12085 const obstacles = frame.obstacles ?? [];
12086 const shortest = closestBorderAnchors(edge.from, edge.to);
12087 const visibleAt = (anchor, zIndex, windowId) => zIndex === null || isPointVisible(anchor, zIndex, obstacles, windowId);
12088 let start = null;
12089 if (shortest && visibleAt(
12090 shortest.from,
12091 edge.fromZIndex,
12092 edge.fromWindowId
12093 )) {
12094 start = shortest.from;
12095 }
12096 if (!start) {
12097 start = endpointAnchor(
12098 edge.from,
12099 edge.fromZIndex,
12100 edge.fromWindowId,
12101 obstacles,
12102 shortest ? { x: shortest.to.x, y: shortest.to.y } : centerOf(edge.to)
12103 );
12104 }
12105 let end = null;
12106 if (shortest && visibleAt(shortest.to, edge.toZIndex, edge.toWindowId)) {
12107 end = shortest.to;
12108 }
12109 if (!end) {
12110 end = endpointAnchor(
12111 edge.to,
12112 edge.toZIndex,
12113 edge.toWindowId,
12114 obstacles,
12115 // Aim the target anchor at the resolved source
12116 // anchor so the curve's two ends agree when
12117 // either moved off the shortest pair.
12118 { x: start.x, y: start.y }
12119 );
12120 }
12121 const distance2 = Math.hypot(
12122 end.x - start.x,
12123 end.y - start.y
12124 );
12125 const c1 = controlPoint(start, distance2);
12126 const c2 = controlPoint(end, distance2);
12127 el.path.setAttribute(
12128 "d",
12129 `M ${start.x} ${start.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${end.x} ${end.y}`
12130 );
12131 const markers = surfaces[el.surface].markers;
12132 const variant = edge.focused ? "active" : "normal";
12133 el.path.setAttribute(
12134 "marker-end",
12135 `url(#${markers.dot[variant]})`
12136 );
12137 el.path.setAttribute(
12138 "marker-start",
12139 `url(#${edge.bidirectional ? markers.dot[variant] : markers.port[variant]})`
12140 );
12141 el.group.classList.toggle(
12142 "desktop-mode-window-link--active",
12143 edge.focused
12144 );
12145 }
12146 for (const [key, el] of Array.from(edges)) {
12147 if (!seen.has(key)) {
12148 el.group.remove();
12149 edges.delete(key);
12150 }
12151 }
12152 };
12153 const unsubscribe = ctx.onFrame(draw);
12154 draw(ctx.getFrame());
12155 return () => {
12156 unsubscribe();
12157 edges.clear();
12158 surfaces.base.svg.remove();
12159 surfaces.elevated.svg.remove();
12160 };
12161 }
12162 });
12163 const LAYER_ID = "desktop-mode-window-links";
12164 const LINKED_CLASS = "desktop-mode-window--linked";
12165 const VISIBLE_CLASS = "desktop-mode-window-links--visible";
12166 let _started$2 = false;
12167 function startWindowLinkRenderHost({
12168 manager: manager2,
12169 osSettings
12170 }) {
12171 if (_started$2) {
12172 return;
12173 }
12174 _started$2 = true;
12175 let snapshot = osSettings.getOsSettingsSnapshot();
12176 let layer = null;
12177 let elevatedLayer = null;
12178 let mountedId = null;
12179 let teardown = null;
12180 let mountToken = 0;
12181 const frameSubscribers = /* @__PURE__ */ new Set();
12182 let framePending = false;
12183 const linkedWindows = /* @__PURE__ */ new Set();
12184 let overviewActive = false;
12185 const rectOf = (win) => {
12186 const el = win.element;
12187 if (!el || !el.isConnected || win.state === "minimized" || // Hidden desktops / display-suppressed windows measure 0×0
12188 // and have no offsetParent — skip their edges entirely.
12189 el.offsetParent === null) {
12190 return null;
12191 }
12192 return {
12193 x: el.offsetLeft,
12194 y: el.offsetTop,
12195 width: el.offsetWidth,
12196 height: el.offsetHeight
12197 };
12198 };
12199 const drawableRectOf = (win) => {
12200 if (win.state === "snapped-left" || win.state === "snapped-right") {
12201 return null;
12202 }
12203 return rectOf(win);
12204 };
12205 const buildFrame2 = () => {
12206 const groups = [];
12207 for (const group of listWindowLinkGroups()) {
12208 if (group.rootWindowIds.length === 0 || group.children.length === 0) {
12209 continue;
12210 }
12211 const members = [];
12212 const push = (windowId, role, content) => {
12213 const win = manager2.getById(windowId);
12214 if (!win || !content) {
12215 return;
12216 }
12217 members.push({
12218 windowId,
12219 role,
12220 content,
12221 rect: drawableRectOf(win),
12222 focused: win.isFocused(),
12223 state: win.state
12224 });
12225 };
12226 for (const id of group.rootWindowIds) {
12227 push(id, "root", getWindowContent(id));
12228 }
12229 for (const child of group.children) {
12230 push(child.windowId, "child", child.content);
12231 }
12232 if (members.length > 0) {
12233 groups.push({ key: group.key, root: group.root, members });
12234 }
12235 }
12236 const zOf = (win) => {
12237 const z = Number.parseInt(
12238 win.element?.style.zIndex || "",
12239 10
12240 );
12241 return Number.isFinite(z) ? z : null;
12242 };
12243 const focusedId = manager2.getFocused()?.id ?? null;
12244 const edges = [];
12245 for (const edge of listWindowLinkEdges()) {
12246 const fromWin = manager2.getById(edge.fromWindowId);
12247 const toWin = manager2.getById(edge.toWindowId);
12248 if (!fromWin || !toWin) {
12249 continue;
12250 }
12251 const focused = fromWin.isFocused() || toWin.isFocused();
12252 edges.push({
12253 fromWindowId: edge.fromWindowId,
12254 toWindowId: edge.toWindowId,
12255 kind: edge.kind,
12256 bidirectional: edge.bidirectional,
12257 focused,
12258 from: drawableRectOf(fromWin),
12259 to: drawableRectOf(toWin),
12260 fromZIndex: zOf(fromWin),
12261 toZIndex: zOf(toWin),
12262 // Only ties TOUCHING the focused window ride the
12263 // elevated layer — an edge between two unfocused
12264 // windows must never draw over a window that happens
12265 // to share a group with the focused one.
12266 elevated: focusedId !== null && (edge.fromWindowId === focusedId || edge.toWindowId === focusedId)
12267 });
12268 }
12269 const obstacles = [];
12270 for (const win of manager2.getAll()) {
12271 const rect = rectOf(win);
12272 if (!rect) {
12273 continue;
12274 }
12275 obstacles.push({
12276 windowId: win.id,
12277 rect,
12278 zIndex: zOf(win) ?? 0
12279 });
12280 }
12281 return {
12282 groups,
12283 edges,
12284 obstacles,
12285 container: {
12286 width: layer?.offsetWidth ?? 0,
12287 height: layer?.offsetHeight ?? 0
12288 }
12289 };
12290 };
12291 const emitFrame = () => {
12292 if (framePending || frameSubscribers.size === 0) {
12293 return;
12294 }
12295 framePending = true;
12296 requestAnimationFrame(() => {
12297 framePending = false;
12298 if (!mountedId) {
12299 return;
12300 }
12301 const frame = buildFrame2();
12302 for (const cb of Array.from(frameSubscribers)) {
12303 try {
12304 cb(frame);
12305 } catch (err) {
12306 if (typeof console !== "undefined") {
12307 console.error(
12308 "[desktop-mode] window-link frame subscriber threw:",
12309 err
12310 );
12311 }
12312 }
12313 }
12314 });
12315 };
12316 const ensureLayer = () => {
12317 if (layer && layer.isConnected && elevatedLayer?.isConnected) {
12318 return layer;
12319 }
12320 const area = document.getElementById("desktop-mode-area");
12321 if (!area) {
12322 return null;
12323 }
12324 layer = document.createElement("div");
12325 layer.id = LAYER_ID;
12326 layer.className = "desktop-mode-window-links";
12327 layer.setAttribute("aria-hidden", "true");
12328 elevatedLayer = document.createElement("div");
12329 elevatedLayer.id = `${LAYER_ID}-elevated`;
12330 elevatedLayer.className = "desktop-mode-window-links desktop-mode-window-links--elevated";
12331 elevatedLayer.setAttribute("aria-hidden", "true");
12332 const widgets = document.getElementById("desktop-mode-widgets");
12333 if (widgets && widgets.parentElement === area) {
12334 widgets.insertAdjacentElement("afterend", elevatedLayer);
12335 widgets.insertAdjacentElement("afterend", layer);
12336 } else {
12337 area.prepend(layer, elevatedLayer);
12338 }
12339 return layer;
12340 };
12341 const isRenderable = () => listWindowLinkEdges().length > 0;
12342 const resolveRendererId = () => {
12343 let id = snapshot.windowLinkRenderer || WINDOW_LINK_RENDERER_DEFAULT;
12344 id = applyFilters(HOOKS.WINDOW_LINK_RENDERER, id);
12345 if (id === WINDOW_LINK_RENDERER_NONE) {
12346 return WINDOW_LINK_RENDERER_NONE;
12347 }
12348 if (getWindowLinkRenderer(id)) {
12349 return id;
12350 }
12351 return getWindowLinkRenderer(WINDOW_LINK_RENDERER_DEFAULT) ? WINDOW_LINK_RENDERER_DEFAULT : WINDOW_LINK_RENDERER_NONE;
12352 };
12353 const unmountRenderer = () => {
12354 mountToken++;
12355 frameSubscribers.clear();
12356 framePending = false;
12357 if (teardown) {
12358 try {
12359 teardown();
12360 } catch (err) {
12361 doAction(HOOKS.SHELL_ERROR, {
12362 scope: "window-link-renderer-teardown",
12363 error: err
12364 });
12365 }
12366 teardown = null;
12367 }
12368 mountedId = null;
12369 layer?.replaceChildren();
12370 elevatedLayer?.replaceChildren();
12371 };
12372 const mountRenderer = (id) => {
12373 const def = getWindowLinkRenderer(id);
12374 const host = ensureLayer();
12375 if (!def || !host || !elevatedLayer) {
12376 return;
12377 }
12378 mountedId = id;
12379 const token = ++mountToken;
12380 const ctx = {
12381 container: host,
12382 elevatedContainer: elevatedLayer,
12383 getFrame: buildFrame2,
12384 onFrame: (cb) => {
12385 frameSubscribers.add(cb);
12386 return () => {
12387 frameSubscribers.delete(cb);
12388 };
12389 }
12390 };
12391 try {
12392 const result = def.mount(ctx);
12393 if (result instanceof Promise) {
12394 result.then((cleanup) => {
12395 if (token !== mountToken) {
12396 if (typeof cleanup === "function") {
12397 cleanup();
12398 }
12399 return;
12400 }
12401 if (typeof cleanup === "function") {
12402 teardown = cleanup;
12403 }
12404 }).catch((err) => {
12405 doAction(HOOKS.SHELL_ERROR, {
12406 scope: "window-link-renderer-mount",
12407 error: err
12408 });
12409 if (token === mountToken) {
12410 mountedId = null;
12411 }
12412 });
12413 } else if (typeof result === "function") {
12414 teardown = result;
12415 }
12416 } catch (err) {
12417 doAction(HOOKS.SHELL_ERROR, {
12418 scope: "window-link-renderer-mount",
12419 error: err
12420 });
12421 mountedId = null;
12422 }
12423 emitFrame();
12424 };
12425 const focusedNeighbors = () => {
12426 const focused = manager2.getFocused();
12427 if (!focused) {
12428 return /* @__PURE__ */ new Set();
12429 }
12430 return new Set(getRelatedWindowIds(focused.id));
12431 };
12432 const isEnabled = () => snapshot.windowLinksEnabled !== false;
12433 const applyVisibility = () => {
12434 if (!layer) {
12435 return;
12436 }
12437 const visible = !overviewActive && isEnabled() && (snapshot.windowLinkVisibility === "always" || snapshot.windowLinkVisibility === "focus" && focusedNeighbors().size > 0);
12438 layer.classList.toggle(VISIBLE_CLASS, visible);
12439 elevatedLayer?.classList.toggle(VISIBLE_CLASS, visible);
12440 };
12441 const raiseRelated = () => {
12442 if (!isEnabled() || snapshot.windowLinkRaiseOnFocus === false || snapshot.windowLinkVisibility === "off") {
12443 return;
12444 }
12445 const focused = manager2.getFocused();
12446 if (!focused) {
12447 return;
12448 }
12449 for (const id of getDirectlyRelatedWindowIds(focused.id)) {
12450 const win = manager2.getById(id);
12451 if (win && win.state !== "minimized") {
12452 manager2.raise(id);
12453 }
12454 }
12455 };
12456 const applyLayerElevation = () => {
12457 if (!elevatedLayer) {
12458 return;
12459 }
12460 const focused = manager2.getFocused();
12461 const related = focusedNeighbors();
12462 if (!focused || related.size === 0 || !isEnabled() || snapshot.windowLinkVisibility === "off") {
12463 elevatedLayer.style.zIndex = "";
12464 return;
12465 }
12466 let maxZ = -Infinity;
12467 for (const id of [focused.id, ...related]) {
12468 const win = manager2.getById(id);
12469 const el = win?.element;
12470 if (!el || win.state === "minimized") {
12471 continue;
12472 }
12473 const z = Number.parseInt(el.style.zIndex || "", 10);
12474 if (Number.isFinite(z)) {
12475 maxZ = Math.max(maxZ, z);
12476 }
12477 }
12478 elevatedLayer.style.zIndex = Number.isFinite(maxZ) ? String(maxZ) : "";
12479 };
12480 const applyLinkedHighlight = () => {
12481 const next = isEnabled() && snapshot.windowLinkHighlight !== false && snapshot.windowLinkVisibility !== "off" ? focusedNeighbors() : /* @__PURE__ */ new Set();
12482 for (const id of linkedWindows) {
12483 if (!next.has(id)) {
12484 manager2.getById(id)?.element?.classList.remove(LINKED_CLASS);
12485 }
12486 }
12487 for (const id of next) {
12488 manager2.getById(id)?.element?.classList.add(LINKED_CLASS);
12489 }
12490 linkedWindows.clear();
12491 for (const id of next) {
12492 linkedWindows.add(id);
12493 }
12494 };
12495 const recompute = () => {
12496 const wantedId = isEnabled() && snapshot.windowLinkVisibility !== "off" && isRenderable() ? resolveRendererId() : WINDOW_LINK_RENDERER_NONE;
12497 if (wantedId === WINDOW_LINK_RENDERER_NONE) {
12498 if (mountedId) {
12499 unmountRenderer();
12500 }
12501 } else if (wantedId !== mountedId) {
12502 unmountRenderer();
12503 mountRenderer(wantedId);
12504 }
12505 applyVisibility();
12506 applyLinkedHighlight();
12507 applyLayerElevation();
12508 emitFrame();
12509 };
12510 addAction(
12511 HOOKS.WINDOW_BOUNDS_CHANGED,
12512 "desktop-mode/window-links-frame",
12513 () => emitFrame()
12514 );
12515 for (const hook of [
12516 HOOKS.WINDOW_MOVED,
12517 HOOKS.WINDOW_RESIZED,
12518 HOOKS.WINDOW_MINIMIZED,
12519 HOOKS.WINDOW_RESTORED,
12520 HOOKS.WINDOW_MAXIMIZED,
12521 HOOKS.WINDOW_UNMAXIMIZED,
12522 HOOKS.WINDOW_FULLSCREEN_ENTERED,
12523 HOOKS.WINDOW_FULLSCREEN_EXITED,
12524 HOOKS.SNAP_ZONE_COMMITTED,
12525 HOOKS.SNAP_SPLIT_FILLED,
12526 HOOKS.DESKTOP_SWITCHED,
12527 HOOKS.SHELL_RESIZED
12528 ]) {
12529 addAction(
12530 hook,
12531 "desktop-mode/window-links-frame",
12532 () => emitFrame()
12533 );
12534 }
12535 addAction(
12536 HOOKS.WINDOW_FOCUSED,
12537 "desktop-mode/window-links-focus",
12538 () => {
12539 raiseRelated();
12540 applyVisibility();
12541 applyLinkedHighlight();
12542 applyLayerElevation();
12543 emitFrame();
12544 }
12545 );
12546 addAction(
12547 HOOKS.WINDOW_BLURRED,
12548 "desktop-mode/window-links-blur",
12549 () => {
12550 applyVisibility();
12551 applyLinkedHighlight();
12552 applyLayerElevation();
12553 emitFrame();
12554 }
12555 );
12556 addAction(
12557 HOOKS.OVERVIEW_ENTERING,
12558 "desktop-mode/window-links-overview",
12559 () => {
12560 overviewActive = true;
12561 applyVisibility();
12562 }
12563 );
12564 addAction(
12565 HOOKS.OVERVIEW_EXITED,
12566 "desktop-mode/window-links-overview",
12567 () => {
12568 overviewActive = false;
12569 applyVisibility();
12570 emitFrame();
12571 }
12572 );
12573 subscribeWindowLinks(recompute);
12574 subscribeWindowLinkRenderers(recompute);
12575 osSettings.subscribeOsSettings((next) => {
12576 const rendererChanged = next.windowLinkRenderer !== snapshot.windowLinkRenderer;
12577 const anyChanged = rendererChanged || next.windowLinkVisibility !== snapshot.windowLinkVisibility || next.windowLinksEnabled !== snapshot.windowLinksEnabled || next.windowLinkRaiseOnFocus !== snapshot.windowLinkRaiseOnFocus || next.windowLinkHighlight !== snapshot.windowLinkHighlight;
12578 snapshot = next;
12579 if (anyChanged) {
12580 if (rendererChanged && mountedId) {
12581 unmountRenderer();
12582 }
12583 recompute();
12584 }
12585 });
12586 recompute();
12587 }
12588 function groupRank(group) {
12589 if (group === "comments") {
12590 return 0;
12591 }
12592 if (group.startsWith("terms/")) {
12593 return 1;
12594 }
12595 if (group === "media") {
12596 return 2;
12597 }
12598 if (group === "links") {
12599 return 3;
12600 }
12601 return 4;
12602 }
12603 function buildRelatedMenu({
12604 items,
12605 onPick
12606 }) {
12607 const panel2 = document.createElement("wpd-menu");
12608 panel2.classList.add("desktop-mode-window__menu-panel");
12609 panel2.classList.add("desktop-mode-window__related-panel");
12610 const groups = /* @__PURE__ */ new Map();
12611 for (const item of items) {
12612 const bucket2 = groups.get(item.group);
12613 if (bucket2) {
12614 bucket2.push(item);
12615 } else {
12616 groups.set(item.group, [item]);
12617 }
12618 }
12619 const ordered = Array.from(groups.entries()).sort(
12620 (a, b) => groupRank(a[0]) - groupRank(b[0])
12621 );
12622 const rows = [];
12623 for (const [, groupItems] of ordered) {
12624 const groupLabel = groupItems.find(
12625 (item) => typeof item.groupLabel === "string" && item.groupLabel !== ""
12626 )?.groupLabel;
12627 if (groupLabel) {
12628 const header = document.createElement("div");
12629 header.className = "desktop-mode-window__related-group";
12630 header.setAttribute("role", "presentation");
12631 header.textContent = groupLabel;
12632 panel2.appendChild(header);
12633 }
12634 for (const item of groupItems) {
12635 const row = document.createElement("wpd-menu-item");
12636 row.setAttribute("role", "menuitem");
12637 row.setAttribute("value", item.id);
12638 row.tabIndex = -1;
12639 if (item.icon) {
12640 row.setAttribute("icon", item.icon);
12641 }
12642 row.classList.add("desktop-mode-window__related-item");
12643 row.textContent = typeof item.count === "number" ? `${item.label} (${item.count})` : item.label;
12644 row.addEventListener("wpd-menu-item-click", (e) => {
12645 e.stopPropagation();
12646 onPick(item);
12647 });
12648 rows.push(row);
12649 panel2.appendChild(row);
12650 }
12651 }
12652 panel2.addEventListener("keydown", (e) => {
12653 const kev = e;
12654 const active2 = rows.indexOf(
12655 panel2.ownerDocument.activeElement
12656 );
12657 if (kev.key === "ArrowDown" || kev.key === "ArrowUp") {
12658 kev.preventDefault();
12659 kev.stopPropagation();
12660 const down = kev.key === "ArrowDown";
12661 let next = rows[down ? 0 : rows.length - 1];
12662 if (active2 !== -1) {
12663 const step = down ? 1 : -1;
12664 next = rows[(active2 + step + rows.length) % rows.length];
12665 }
12666 next?.focus();
12667 } else if (kev.key === "Home" || kev.key === "End") {
12668 kev.preventDefault();
12669 kev.stopPropagation();
12670 rows[kev.key === "Home" ? 0 : rows.length - 1]?.focus();
12671 } else if (kev.key === "Enter" || kev.key === " ") {
12672 const row = rows[active2];
12673 if (row) {
12674 kev.preventDefault();
12675 kev.stopPropagation();
12676 row.dispatchEvent(
12677 new CustomEvent("wpd-menu-item-click", {
12678 bubbles: true
12679 })
12680 );
12681 }
12682 }
12683 });
12684 return panel2;
12685 }
12686 function isValidItem(item) {
12687 if (!item || typeof item !== "object") {
12688 return false;
12689 }
12690 const candidate = item;
12691 const requiredString = (v) => typeof v === "string" && v.trim() !== "";
12692 return requiredString(candidate.id) && requiredString(candidate.group) && requiredString(candidate.label) && requiredString(candidate.url) && (candidate.groupLabel === void 0 || typeof candidate.groupLabel === "string") && (candidate.icon === void 0 || typeof candidate.icon === "string") && (candidate.count === void 0 || typeof candidate.count === "number" && Number.isFinite(candidate.count));
12693 }
12694 function resolveRelatedItems(windowId) {
12695 const content = getWindowContent(windowId) ?? null;
12696 const base = content && Array.isArray(content.related) ? content.related.map((item) => ({ ...item })) : [];
12697 const filtered = applyFilters(
12698 HOOKS.RELATED_ENTITIES_ITEMS,
12699 base,
12700 { windowId, content }
12701 );
12702 if (!Array.isArray(filtered)) {
12703 if (typeof console !== "undefined") {
12704 console.warn(
12705 "[desktop-mode] `desktop-mode.related-entities.items` filter returned a non-array; falling back to the identity list."
12706 );
12707 }
12708 return base.filter(isValidItem);
12709 }
12710 return filtered.filter(isValidItem);
12711 }
12712 function closePanels(root) {
12713 root?.querySelectorAll(
12714 ".desktop-mode-window__related-panel"
12715 ).forEach((el) => {
12716 if (el._wpdRelatedClose) {
12717 el._wpdRelatedClose();
12718 } else {
12719 el.remove();
12720 }
12721 });
12722 }
12723 function suppressNextDblclick(titleBar) {
12724 const swallow = (e) => {
12725 e.stopImmediatePropagation();
12726 };
12727 titleBar.addEventListener("dblclick", swallow, true);
12728 setTimeout(() => {
12729 titleBar.removeEventListener("dblclick", swallow, true);
12730 }, 500);
12731 }
12732 function openRelatedMenu(host, win, openUrl) {
12733 const titleBar = host.closest(
12734 ".desktop-mode-window__titlebar"
12735 );
12736 if (!titleBar) {
12737 return;
12738 }
12739 const items = resolveRelatedItems(win.id);
12740 if (items.length === 0) {
12741 return;
12742 }
12743 let onDocPointerDown = null;
12744 const close = () => {
12745 if (onDocPointerDown) {
12746 document.removeEventListener("pointerdown", onDocPointerDown, true);
12747 onDocPointerDown = null;
12748 }
12749 titleBar.removeEventListener("keydown", onTitleBarKeydown);
12750 panel2.remove();
12751 host.setAttribute("aria-expanded", "false");
12752 };
12753 const onTitleBarKeydown = (e) => {
12754 if (e.key === "Escape") {
12755 e.stopPropagation();
12756 close();
12757 host.focus();
12758 }
12759 };
12760 const panel2 = buildRelatedMenu({
12761 items,
12762 onPick: (item) => {
12763 close();
12764 suppressNextDblclick(titleBar);
12765 openUrl(item);
12766 }
12767 });
12768 panel2._wpdRelatedClose = close;
12769 titleBar.appendChild(panel2);
12770 titleBar.addEventListener("keydown", onTitleBarKeydown);
12771 host.setAttribute("aria-expanded", "true");
12772 onDocPointerDown = (e) => {
12773 const target2 = e.target;
12774 if (!target2 || panel2.contains(target2) || host.contains(target2)) {
12775 return;
12776 }
12777 close();
12778 };
12779 setTimeout(() => {
12780 if (onDocPointerDown) {
12781 document.addEventListener("pointerdown", onDocPointerDown, true);
12782 }
12783 }, 0);
12784 panel2.querySelector('[role="menuitem"]')?.focus();
12785 }
12786 function bootRelatedEntities({
12787 manager: manager2,
12788 openUrl
12789 }) {
12790 registerTitleBarButton({
12791 id: "desktop-mode/related-entities",
12792 label: __("Related"),
12793 icon: "dashicons-networking",
12794 placement: "right",
12795 order: 60,
12796 match: (win) => resolveRelatedItems(win.id).length > 0,
12797 render: (host, win) => {
12798 closePanels(win.element);
12799 host.setAttribute("aria-haspopup", "menu");
12800 host.setAttribute("aria-expanded", "false");
12801 host.addEventListener("click", (e) => {
12802 e.stopPropagation();
12803 const open = win.element?.querySelector(
12804 ".desktop-mode-window__related-panel"
12805 );
12806 if (open) {
12807 closePanels(win.element);
12808 return;
12809 }
12810 openRelatedMenu(host, win, openUrl);
12811 });
12812 }
12813 });
12814 addAction(
12815 HOOKS.WINDOW_CONTENT_CHANGED,
12816 "desktop-mode/related-entities",
12817 (e) => {
12818 if (!e?.windowId) {
12819 return;
12820 }
12821 const win = manager2.getById(e.windowId);
12822 if (!win) {
12823 return;
12824 }
12825 closePanels(win.element);
12826 win.renderCustomTitleBarButtons?.();
12827 }
12828 );
12829 }
12830 const UNFOCUS_EFFECT_NONE = "none";
12831 const store$9 = createSharedStore(
12832 "desktop-mode/unfocus-effect-registry",
12833 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
12834 );
12835 const registry$4 = store$9.state.registry;
12836 const listeners$7 = store$9.state.listeners;
12837 const UNFOCUS_EFFECT_ID = /^[a-z0-9_/-]+$/;
12838 function registerUnfocusEffect(def) {
12839 const errors = [];
12840 if (!def || typeof def !== "object") {
12841 errors.push("def (not an object)");
12842 } else {
12843 if (typeof def.id !== "string" || def.id.trim() === "") {
12844 errors.push("id (missing)");
12845 } else if (!UNFOCUS_EFFECT_ID.test(def.id.trim().toLowerCase())) {
12846 errors.push(
12847 `id (must match ${UNFOCUS_EFFECT_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
12848 );
12849 } else if (def.id.trim().toLowerCase() === UNFOCUS_EFFECT_NONE) {
12850 errors.push('id ("none" is reserved)');
12851 }
12852 if (typeof def.label !== "string" || def.label.trim() === "") {
12853 errors.push("label (missing)");
12854 }
12855 if (typeof def.className !== "string" && typeof def.apply !== "function") {
12856 errors.push(
12857 "className|apply (at least one must be provided — a CSS class to toggle or an apply callback)"
12858 );
12859 }
12860 }
12861 throwOnRegistrationErrors("UnfocusEffect", errors, def);
12862 const id = def.id.trim().toLowerCase();
12863 registry$4.set(id, { ...def, id });
12864 notify$7();
12865 }
12866 function unregisterUnfocusEffect(id) {
12867 if (registry$4.delete(id.toLowerCase())) {
12868 notify$7();
12869 }
12870 }
12871 function unregisterUnfocusEffectsByOwner(owner) {
12872 if (!owner) {
12873 return 0;
12874 }
12875 let removed = 0;
12876 for (const [id, def] of Array.from(registry$4.entries())) {
12877 if (def.owner === owner) {
12878 registry$4.delete(id);
12879 removed++;
12880 }
12881 }
12882 if (removed > 0) {
12883 notify$7();
12884 }
12885 return removed;
12886 }
12887 function listUnfocusEffects() {
12888 const copy = Array.from(registry$4.values());
12889 const filtered = applyFilters(
12890 HOOKS.UNFOCUS_EFFECTS,
12891 copy
12892 );
12893 if (!Array.isArray(filtered)) {
12894 if (typeof console !== "undefined") {
12895 console.warn(
12896 "[desktop-mode] `desktop-mode.unfocus-effects` filter returned a non-array; falling back to registry list."
12897 );
12898 }
12899 return copy;
12900 }
12901 return filtered;
12902 }
12903 function getUnfocusEffect(id) {
12904 return listUnfocusEffects().find((e) => e.id === id);
12905 }
12906 function subscribeUnfocusEffects(cb) {
12907 listeners$7.add(cb);
12908 return () => {
12909 listeners$7.delete(cb);
12910 };
12911 }
12912 function notify$7() {
12913 const snapshot = Array.from(listeners$7);
12914 for (const cb of snapshot) {
12915 try {
12916 cb();
12917 } catch (err) {
12918 if (typeof console !== "undefined") {
12919 console.error(
12920 "[desktop-mode] unfocus-effect registry listener threw:",
12921 err
12922 );
12923 }
12924 }
12925 }
12926 }
12927 registerUnfocusEffect({
12928 id: "darken",
12929 label: __("Darken"),
12930 description: __("Dim unfocused windows so the focused one stands out."),
12931 className: "desktop-mode-window--fx-darken"
12932 });
12933 registerUnfocusEffect({
12934 id: "frost",
12935 label: __("Frost"),
12936 description: __(
12937 "Throw unfocused windows out of focus — a soft, frosted-glass blur, as if you were looking at them through an iced-over pane."
12938 ),
12939 className: "desktop-mode-window--fx-frost"
12940 });
12941 registerUnfocusEffect({
12942 id: "grayscale",
12943 label: __("Grayscale"),
12944 description: __(
12945 "Drain the colour from unfocused windows so the focused one is the only thing still in colour — your eye snaps right to it."
12946 ),
12947 className: "desktop-mode-window--fx-grayscale"
12948 });
12949 function createUnfocusEffectRegistrySync() {
12950 const loadedHandles = /* @__PURE__ */ new Set();
12951 const loadedUrls = /* @__PURE__ */ new Set();
12952 const ensureScript = async (entry) => {
12953 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
12954 loadedHandles.add(entry.handle);
12955 return;
12956 }
12957 try {
12958 await loadVendorScript(entry.scriptUrl, {
12959 translations: entry.scriptTranslations,
12960 l10n: entry.scriptL10n,
12961 before: entry.scriptBefore,
12962 after: entry.scriptAfter
12963 });
12964 } catch (err) {
12965 doAction(HOOKS.SHELL_ERROR, {
12966 scope: "unfocus-effect-script-load",
12967 handle: entry.handle,
12968 url: entry.scriptUrl,
12969 error: err
12970 });
12971 return;
12972 }
12973 loadedUrls.add(entry.scriptUrl);
12974 loadedHandles.add(entry.handle);
12975 };
12976 return async (scripts) => {
12977 const incomingHandles = /* @__PURE__ */ new Set();
12978 for (const entry of scripts) {
12979 if (entry.handle) {
12980 incomingHandles.add(entry.handle);
12981 }
12982 }
12983 for (const handle of Array.from(loadedHandles)) {
12984 if (incomingHandles.has(handle)) {
12985 continue;
12986 }
12987 unregisterUnfocusEffectsByOwner(handle);
12988 loadedHandles.delete(handle);
12989 }
12990 for (const entry of scripts) {
12991 if (!entry.handle || loadedHandles.has(entry.handle)) {
12992 continue;
12993 }
12994 await ensureScript(entry);
12995 }
12996 };
12997 }
12998 function createWindowLinkRendererRegistrySync() {
12999 const loadedHandles = /* @__PURE__ */ new Set();
13000 const loadedUrls = /* @__PURE__ */ new Set();
13001 const ensureScript = async (entry) => {
13002 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
13003 loadedHandles.add(entry.handle);
13004 return;
13005 }
13006 try {
13007 await loadVendorScript(entry.scriptUrl, {
13008 translations: entry.scriptTranslations,
13009 l10n: entry.scriptL10n,
13010 before: entry.scriptBefore,
13011 after: entry.scriptAfter
13012 });
13013 } catch (err) {
13014 doAction(HOOKS.SHELL_ERROR, {
13015 scope: "window-link-renderer-script-load",
13016 handle: entry.handle,
13017 url: entry.scriptUrl,
13018 error: err
13019 });
13020 return;
13021 }
13022 loadedUrls.add(entry.scriptUrl);
13023 loadedHandles.add(entry.handle);
13024 };
13025 return async (scripts) => {
13026 const incomingHandles = /* @__PURE__ */ new Set();
13027 for (const entry of scripts) {
13028 if (entry.handle) {
13029 incomingHandles.add(entry.handle);
13030 }
13031 }
13032 for (const handle of Array.from(loadedHandles)) {
13033 if (incomingHandles.has(handle)) {
13034 continue;
13035 }
13036 unregisterWindowLinkRenderersByOwner(handle);
13037 loadedHandles.delete(handle);
13038 }
13039 for (const entry of scripts) {
13040 if (!entry.handle || loadedHandles.has(entry.handle)) {
13041 continue;
13042 }
13043 await ensureScript(entry);
13044 }
13045 };
13046 }
13047 const EFFECT_ATTR = "data-desktop-unfocus-effect";
13048 const EFFECT_CLASS_ATTR = "data-desktop-unfocus-effect-class";
13049 let _started$1 = false;
13050 function hostsCanvas(el) {
13051 return el.querySelector("canvas") !== null;
13052 }
13053 function startUnfocusEngine({ manager: manager2, osSettings }) {
13054 if (_started$1) {
13055 return;
13056 }
13057 _started$1 = true;
13058 let currentId = osSettings.getOsSettingsSnapshot().unfocusEffect;
13059 const clear = (el, allEffects) => {
13060 const storedClass = el.getAttribute(EFFECT_CLASS_ATTR);
13061 if (storedClass) {
13062 el.classList.remove(storedClass);
13063 el.removeAttribute(EFFECT_CLASS_ATTR);
13064 }
13065 const priorId = el.getAttribute(EFFECT_ATTR);
13066 if (priorId) {
13067 getUnfocusEffect(priorId)?.clear?.(el);
13068 }
13069 for (const def of allEffects) {
13070 if (def.className) {
13071 el.classList.remove(def.className);
13072 }
13073 }
13074 el.removeAttribute(EFFECT_ATTR);
13075 };
13076 const apply = (el, def) => {
13077 if (def.className) {
13078 el.classList.add(def.className);
13079 el.setAttribute(EFFECT_CLASS_ATTR, def.className);
13080 }
13081 el.setAttribute(EFFECT_ATTR, def.id);
13082 def.apply?.(el);
13083 };
13084 const recompute = () => {
13085 const def = currentId === UNFOCUS_EFFECT_NONE ? void 0 : getUnfocusEffect(currentId);
13086 const allEffects = listUnfocusEffects();
13087 for (const win of manager2.getAll()) {
13088 const el = win.element;
13089 if (!el) {
13090 continue;
13091 }
13092 clear(el, allEffects);
13093 if (!def || win.isFocused() || win.state === "minimized") {
13094 continue;
13095 }
13096 if (hostsCanvas(el)) {
13097 continue;
13098 }
13099 apply(el, def);
13100 }
13101 };
13102 for (const name of [
13103 "desktop-mode-window-opened",
13104 "desktop-mode-window-reopened",
13105 "desktop-mode-window-closed",
13106 "desktop-mode-window-focused",
13107 "desktop-mode-window-blurred"
13108 ]) {
13109 document.addEventListener(name, () => recompute());
13110 }
13111 osSettings.subscribeOsSettings((snapshot) => {
13112 currentId = snapshot.unfocusEffect;
13113 recompute();
13114 });
13115 subscribeUnfocusEffects(() => recompute());
13116 recompute();
13117 }
13118 function createDockRailRendererSync() {
13119 const loadedHandles = /* @__PURE__ */ new Set();
13120 const loadedUrls = /* @__PURE__ */ new Set();
13121 const ensureScript = async (entry) => {
13122 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
13123 loadedHandles.add(entry.handle);
13124 return;
13125 }
13126 try {
13127 await loadVendorScript(entry.scriptUrl, {
13128 translations: entry.scriptTranslations,
13129 l10n: entry.scriptL10n,
13130 before: entry.scriptBefore,
13131 after: entry.scriptAfter
13132 });
13133 } catch (err) {
13134 doAction(HOOKS.SHELL_ERROR, {
13135 scope: "dock-rail-renderer-script-load",
13136 handle: entry.handle,
13137 url: entry.scriptUrl,
13138 error: err
13139 });
13140 return;
13141 }
13142 loadedUrls.add(entry.scriptUrl);
13143 loadedHandles.add(entry.handle);
13144 };
13145 return async (scripts) => {
13146 const incomingHandles = /* @__PURE__ */ new Set();
13147 for (const entry of scripts) {
13148 if (entry.handle) {
13149 incomingHandles.add(entry.handle);
13150 }
13151 }
13152 for (const handle of Array.from(loadedHandles)) {
13153 if (incomingHandles.has(handle)) {
13154 continue;
13155 }
13156 unregisterByOwner$1(handle);
13157 loadedHandles.delete(handle);
13158 }
13159 for (const entry of scripts) {
13160 if (!entry.handle || loadedHandles.has(entry.handle)) {
13161 continue;
13162 }
13163 await ensureScript(entry);
13164 }
13165 };
13166 }
13167 const store$8 = createSharedStore(
13168 "desktop-mode/window-themes-registry",
13169 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
13170 );
13171 const registry$3 = store$8.state.registry;
13172 const listeners$6 = store$8.state.listeners;
13173 const WINDOW_THEME_ID = /^[a-z0-9_/-]+$/;
13174 function registerWindowTheme(def) {
13175 const errors = [];
13176 if (!def || typeof def !== "object") {
13177 errors.push("def (not an object)");
13178 } else {
13179 if (typeof def.id !== "string" || def.id.trim() === "") {
13180 errors.push("id (missing)");
13181 } else if (!WINDOW_THEME_ID.test(def.id.trim().toLowerCase())) {
13182 errors.push(
13183 `id (must match ${WINDOW_THEME_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
13184 );
13185 }
13186 if (!def.tokens || typeof def.tokens !== "object") {
13187 errors.push("tokens (must be an object of CSS custom-property → value)");
13188 } else {
13189 for (const key of Object.keys(def.tokens)) {
13190 if (!key.startsWith("--")) {
13191 errors.push(
13192 `tokens.${key} (CSS custom-property keys must start with "--")`
13193 );
13194 break;
13195 }
13196 }
13197 }
13198 if (typeof def.match !== "function") {
13199 errors.push("match (must be a function)");
13200 }
13201 }
13202 throwOnRegistrationErrors("WindowTheme", errors, def);
13203 const id = def.id.trim().toLowerCase();
13204 registry$3.set(id, { ...def, id });
13205 notify$6();
13206 }
13207 function unregisterWindowTheme(id) {
13208 if (registry$3.delete(id.toLowerCase())) {
13209 notify$6();
13210 }
13211 }
13212 function unregisterWindowThemesByOwner(owner) {
13213 if (!owner) {
13214 return 0;
13215 }
13216 let removed = 0;
13217 for (const [id, def] of Array.from(registry$3.entries())) {
13218 if (def.owner === owner) {
13219 registry$3.delete(id);
13220 removed++;
13221 }
13222 }
13223 if (removed > 0) {
13224 notify$6();
13225 }
13226 return removed;
13227 }
13228 function listWindowThemes() {
13229 return Array.from(registry$3.values()).sort(
13230 (a, b) => (a.priority ?? 100) - (b.priority ?? 100)
13231 );
13232 }
13233 function notify$6() {
13234 const snapshot = Array.from(listeners$6);
13235 for (const cb of snapshot) {
13236 try {
13237 cb();
13238 } catch (err) {
13239 if (typeof console !== "undefined") {
13240 console.error(
13241 "[desktop-mode] window-theme registry listener threw:",
13242 err
13243 );
13244 }
13245 }
13246 }
13247 }
13248 function createWindowThemeRegistrySync() {
13249 const loadedHandles = /* @__PURE__ */ new Set();
13250 const loadedUrls = /* @__PURE__ */ new Set();
13251 let prevIdsByHandle = /* @__PURE__ */ new Map();
13252 const shellRegistered = /* @__PURE__ */ new Set();
13253 const ensureScript = async (entry) => {
13254 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
13255 loadedHandles.add(entry.handle);
13256 return;
13257 }
13258 try {
13259 await loadVendorScript(entry.scriptUrl, {
13260 translations: entry.scriptTranslations,
13261 l10n: entry.scriptL10n,
13262 before: entry.scriptBefore,
13263 after: entry.scriptAfter
13264 });
13265 } catch (err) {
13266 doAction(HOOKS.SHELL_ERROR, {
13267 scope: "window-theme-script-load",
13268 handle: entry.handle,
13269 url: entry.scriptUrl,
13270 error: err
13271 });
13272 return;
13273 }
13274 loadedUrls.add(entry.scriptUrl);
13275 loadedHandles.add(entry.handle);
13276 };
13277 const idsByHandleFrom = (themes) => {
13278 const map = /* @__PURE__ */ new Map();
13279 if (!themes) {
13280 return map;
13281 }
13282 for (const entry of themes) {
13283 if (!entry.scriptHandle || !entry.id) {
13284 continue;
13285 }
13286 let set = map.get(entry.scriptHandle);
13287 if (!set) {
13288 set = /* @__PURE__ */ new Set();
13289 map.set(entry.scriptHandle, set);
13290 }
13291 set.add(entry.id);
13292 }
13293 return map;
13294 };
13295 const collectIdsToRemove = (handle) => {
13296 const ids = /* @__PURE__ */ new Set();
13297 for (const def of listWindowThemes()) {
13298 if (def.owner === handle) {
13299 ids.add(def.id);
13300 }
13301 }
13302 const declared = prevIdsByHandle.get(handle);
13303 if (declared) {
13304 for (const id of declared) {
13305 ids.add(id);
13306 }
13307 }
13308 return ids;
13309 };
13310 const applyMetadata = (themes) => {
13311 if (!themes) {
13312 return;
13313 }
13314 for (const entry of themes) {
13315 if (!entry.id || !entry.tokens) {
13316 continue;
13317 }
13318 try {
13319 registerWindowTheme({
13320 id: entry.id,
13321 label: entry.label,
13322 tokens: entry.tokens,
13323 priority: entry.priority,
13324 match: () => true,
13325 owner: entry.scriptHandle || void 0
13326 });
13327 shellRegistered.add(entry.id);
13328 } catch (err) {
13329 doAction(HOOKS.SHELL_ERROR, {
13330 scope: "window-theme-shell-register",
13331 id: entry.id,
13332 error: err
13333 });
13334 }
13335 }
13336 };
13337 return async (scripts, themes) => {
13338 const incomingHandles = /* @__PURE__ */ new Set();
13339 for (const entry of scripts) {
13340 if (entry.handle) {
13341 incomingHandles.add(entry.handle);
13342 }
13343 }
13344 for (const handle of Array.from(loadedHandles)) {
13345 if (incomingHandles.has(handle)) {
13346 continue;
13347 }
13348 const ids = collectIdsToRemove(handle);
13349 for (const id of ids) {
13350 unregisterWindowTheme(id);
13351 shellRegistered.delete(id);
13352 }
13353 unregisterWindowThemesByOwner(handle);
13354 loadedHandles.delete(handle);
13355 }
13356 applyMetadata(themes);
13357 for (const entry of scripts) {
13358 if (!entry.handle || loadedHandles.has(entry.handle)) {
13359 continue;
13360 }
13361 await ensureScript(entry);
13362 }
13363 prevIdsByHandle = idsByHandleFrom(themes);
13364 };
13365 }
13366 const store$7 = createSharedStore(
13367 "desktop-mode/window-controls-registry",
13368 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
13369 );
13370 const registry$2 = store$7.state.registry;
13371 const listeners$5 = store$7.state.listeners;
13372 const WINDOW_CONTROL_ID = /^[a-z0-9_/-]+$/;
13373 function registerWindowControl(def) {
13374 const errors = [];
13375 if (!def || typeof def !== "object") {
13376 errors.push("def (not an object)");
13377 } else {
13378 if (typeof def.id !== "string" || def.id.trim() === "") {
13379 errors.push("id (missing)");
13380 } else if (!WINDOW_CONTROL_ID.test(def.id.trim().toLowerCase())) {
13381 errors.push(
13382 `id (must match ${WINDOW_CONTROL_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
13383 );
13384 }
13385 if (typeof def.label !== "string" || def.label.trim() === "") {
13386 errors.push("label (missing)");
13387 }
13388 if (typeof def.onClick !== "function" && typeof def.render !== "function") {
13389 errors.push("onClick|render (at least one must be a function)");
13390 }
13391 if (typeof def.render !== "function") {
13392 if (typeof def.icon !== "string" || def.icon.trim() === "") {
13393 errors.push("icon (required when render is omitted)");
13394 }
13395 }
13396 if (typeof def.match !== "function") {
13397 errors.push("match (must be a function)");
13398 }
13399 if (def.placement !== void 0 && def.placement !== "left" && def.placement !== "right" && def.placement !== "controls") {
13400 errors.push('placement (must be "left", "right", or "controls")');
13401 }
13402 }
13403 throwOnRegistrationErrors("WindowControl", errors, def);
13404 const id = def.id.trim().toLowerCase();
13405 registry$2.set(id, { ...def, id });
13406 notify$5();
13407 }
13408 function unregisterWindowControl(id) {
13409 if (registry$2.delete(id.toLowerCase())) {
13410 notify$5();
13411 }
13412 }
13413 function unregisterWindowControlsByOwner(owner) {
13414 if (!owner) {
13415 return 0;
13416 }
13417 let removed = 0;
13418 for (const [id, def] of Array.from(registry$2.entries())) {
13419 if (def.owner === owner) {
13420 registry$2.delete(id);
13421 removed++;
13422 }
13423 }
13424 if (removed > 0) {
13425 notify$5();
13426 }
13427 return removed;
13428 }
13429 function listWindowControls() {
13430 return Array.from(registry$2.values()).sort((a, b) => {
13431 const oa = a.order ?? 100;
13432 const ob = b.order ?? 100;
13433 if (oa !== ob) {
13434 return oa - ob;
13435 }
13436 return a.id.localeCompare(b.id);
13437 });
13438 }
13439 function notify$5() {
13440 const snapshot = Array.from(listeners$5);
13441 for (const cb of snapshot) {
13442 try {
13443 cb();
13444 } catch (err) {
13445 if (typeof console !== "undefined") {
13446 console.error(
13447 "[desktop-mode] window-control registry listener threw:",
13448 err
13449 );
13450 }
13451 }
13452 }
13453 }
13454 function registerBuiltInControls() {
13455 registerWindowControl({
13456 id: "core/minimize",
13457 label: __("Minimize"),
13458 icon: "minimize",
13459 placement: "controls",
13460 order: 10,
13461 core: true,
13462 match: () => true,
13463 onClick: (win) => {
13464 win.minimize();
13465 }
13466 });
13467 registerWindowControl({
13468 id: "core/maximize",
13469 label: __("Maximize"),
13470 icon: "maximize",
13471 placement: "controls",
13472 order: 20,
13473 core: true,
13474 match: () => true,
13475 onClick: (win) => {
13476 win.toggleMaximize();
13477 }
13478 });
13479 registerWindowControl({
13480 id: "core/focus-tab",
13481 label: __("Enter fullscreen"),
13482 icon: "fullscreen",
13483 placement: "controls",
13484 order: 30,
13485 core: true,
13486 match: () => true,
13487 onClick: (win) => {
13488 win.toggleFullscreen();
13489 }
13490 });
13491 registerWindowControl({
13492 id: "core/close",
13493 label: __("Close"),
13494 icon: "close",
13495 placement: "controls",
13496 order: 50,
13497 core: true,
13498 match: () => true,
13499 onClick: (win) => {
13500 win.close();
13501 }
13502 });
13503 }
13504 function createWindowControlRegistrySync() {
13505 const loadedHandles = /* @__PURE__ */ new Set();
13506 const loadedUrls = /* @__PURE__ */ new Set();
13507 let prevIdsByHandle = /* @__PURE__ */ new Map();
13508 const ensureScript = async (entry) => {
13509 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
13510 loadedHandles.add(entry.handle);
13511 return;
13512 }
13513 try {
13514 await loadVendorScript(entry.scriptUrl, {
13515 translations: entry.scriptTranslations,
13516 l10n: entry.scriptL10n,
13517 before: entry.scriptBefore,
13518 after: entry.scriptAfter
13519 });
13520 } catch (err) {
13521 doAction(HOOKS.SHELL_ERROR, {
13522 scope: "window-control-script-load",
13523 handle: entry.handle,
13524 url: entry.scriptUrl,
13525 error: err
13526 });
13527 return;
13528 }
13529 loadedUrls.add(entry.scriptUrl);
13530 loadedHandles.add(entry.handle);
13531 };
13532 const idsByHandleFrom = (controls) => {
13533 const map = /* @__PURE__ */ new Map();
13534 if (!controls) {
13535 return map;
13536 }
13537 for (const entry of controls) {
13538 if (!entry.scriptHandle || !entry.id) {
13539 continue;
13540 }
13541 let set = map.get(entry.scriptHandle);
13542 if (!set) {
13543 set = /* @__PURE__ */ new Set();
13544 map.set(entry.scriptHandle, set);
13545 }
13546 set.add(entry.id);
13547 }
13548 return map;
13549 };
13550 const collectIdsToRemove = (handle) => {
13551 const ids = /* @__PURE__ */ new Set();
13552 for (const def of listWindowControls()) {
13553 if (def.owner === handle) {
13554 ids.add(def.id);
13555 }
13556 }
13557 const declared = prevIdsByHandle.get(handle);
13558 if (declared) {
13559 for (const id of declared) {
13560 ids.add(id);
13561 }
13562 }
13563 return ids;
13564 };
13565 return async (scripts, controls) => {
13566 const incomingHandles = /* @__PURE__ */ new Set();
13567 for (const entry of scripts) {
13568 if (entry.handle) {
13569 incomingHandles.add(entry.handle);
13570 }
13571 }
13572 for (const handle of Array.from(loadedHandles)) {
13573 if (incomingHandles.has(handle)) {
13574 continue;
13575 }
13576 for (const id of collectIdsToRemove(handle)) {
13577 unregisterWindowControl(id);
13578 }
13579 unregisterWindowControlsByOwner(handle);
13580 loadedHandles.delete(handle);
13581 }
13582 for (const entry of scripts) {
13583 if (!entry.handle || loadedHandles.has(entry.handle)) {
13584 continue;
13585 }
13586 await ensureScript(entry);
13587 }
13588 prevIdsByHandle = idsByHandleFrom(controls);
13589 };
13590 }
13591 const store$6 = createSharedStore(
13592 "desktop-mode/window-slots-registry",
13593 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
13594 );
13595 const registry$1 = store$6.state.registry;
13596 const listeners$4 = store$6.state.listeners;
13597 const WINDOW_SLOT_ID = /^[a-z0-9_/-]+$/;
13598 const KNOWN_SLOTS = /* @__PURE__ */ new Set([
13599 "before-titlebar",
13600 "before-icon",
13601 "icon",
13602 "title",
13603 "after-title",
13604 "before-controls",
13605 "controls",
13606 "after-controls",
13607 "after-titlebar"
13608 ]);
13609 function registerWindowSlot(def) {
13610 const errors = [];
13611 if (!def || typeof def !== "object") {
13612 errors.push("def (not an object)");
13613 } else {
13614 if (typeof def.id !== "string" || def.id.trim() === "") {
13615 errors.push("id (missing)");
13616 } else if (!WINDOW_SLOT_ID.test(def.id.trim().toLowerCase())) {
13617 errors.push(
13618 `id (must match ${WINDOW_SLOT_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
13619 );
13620 }
13621 if (typeof def.slot !== "string" || def.slot.trim() === "") {
13622 errors.push("slot (missing)");
13623 } else if (!KNOWN_SLOTS.has(def.slot)) {
13624 errors.push(
13625 `slot (must be one of ${Array.from(KNOWN_SLOTS).join(", ")})`
13626 );
13627 }
13628 if (typeof def.match !== "function") {
13629 errors.push("match (must be a function)");
13630 }
13631 if (typeof def.render !== "function") {
13632 errors.push("render (must be a function)");
13633 }
13634 }
13635 throwOnRegistrationErrors("WindowSlot", errors, def);
13636 const id = def.id.trim().toLowerCase();
13637 registry$1.set(id, { ...def, id });
13638 notify$4();
13639 }
13640 function unregisterWindowSlot(id) {
13641 if (registry$1.delete(id.toLowerCase())) {
13642 notify$4();
13643 }
13644 }
13645 function unregisterWindowSlotsByOwner(owner) {
13646 if (!owner) {
13647 return 0;
13648 }
13649 let removed = 0;
13650 for (const [id, def] of Array.from(registry$1.entries())) {
13651 if (def.owner === owner) {
13652 registry$1.delete(id);
13653 removed++;
13654 }
13655 }
13656 if (removed > 0) {
13657 notify$4();
13658 }
13659 return removed;
13660 }
13661 function listWindowSlots() {
13662 return Array.from(registry$1.values()).sort((a, b) => {
13663 const oa = a.order ?? 100;
13664 const ob = b.order ?? 100;
13665 if (oa !== ob) {
13666 return oa - ob;
13667 }
13668 return a.id.localeCompare(b.id);
13669 });
13670 }
13671 function notify$4() {
13672 const snapshot = Array.from(listeners$4);
13673 for (const cb of snapshot) {
13674 try {
13675 cb();
13676 } catch (err) {
13677 if (typeof console !== "undefined") {
13678 console.error(
13679 "[desktop-mode] window-slot registry listener threw:",
13680 err
13681 );
13682 }
13683 }
13684 }
13685 }
13686 function createWindowSlotRegistrySync() {
13687 const loadedHandles = /* @__PURE__ */ new Set();
13688 const loadedUrls = /* @__PURE__ */ new Set();
13689 let prevIdsByHandle = /* @__PURE__ */ new Map();
13690 const ensureScript = async (entry) => {
13691 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
13692 loadedHandles.add(entry.handle);
13693 return;
13694 }
13695 try {
13696 await loadVendorScript(entry.scriptUrl, {
13697 translations: entry.scriptTranslations,
13698 l10n: entry.scriptL10n,
13699 before: entry.scriptBefore,
13700 after: entry.scriptAfter
13701 });
13702 } catch (err) {
13703 doAction(HOOKS.SHELL_ERROR, {
13704 scope: "window-slot-script-load",
13705 handle: entry.handle,
13706 url: entry.scriptUrl,
13707 error: err
13708 });
13709 return;
13710 }
13711 loadedUrls.add(entry.scriptUrl);
13712 loadedHandles.add(entry.handle);
13713 };
13714 const idsByHandleFrom = (slots) => {
13715 const map = /* @__PURE__ */ new Map();
13716 if (!slots) {
13717 return map;
13718 }
13719 for (const entry of slots) {
13720 if (!entry.scriptHandle || !entry.id) {
13721 continue;
13722 }
13723 let set = map.get(entry.scriptHandle);
13724 if (!set) {
13725 set = /* @__PURE__ */ new Set();
13726 map.set(entry.scriptHandle, set);
13727 }
13728 set.add(entry.id);
13729 }
13730 return map;
13731 };
13732 const collectIdsToRemove = (handle) => {
13733 const ids = /* @__PURE__ */ new Set();
13734 for (const def of listWindowSlots()) {
13735 if (def.owner === handle) {
13736 ids.add(def.id);
13737 }
13738 }
13739 const declared = prevIdsByHandle.get(handle);
13740 if (declared) {
13741 for (const id of declared) {
13742 ids.add(id);
13743 }
13744 }
13745 return ids;
13746 };
13747 return async (scripts, slots) => {
13748 const incomingHandles = /* @__PURE__ */ new Set();
13749 for (const entry of scripts) {
13750 if (entry.handle) {
13751 incomingHandles.add(entry.handle);
13752 }
13753 }
13754 for (const handle of Array.from(loadedHandles)) {
13755 if (incomingHandles.has(handle)) {
13756 continue;
13757 }
13758 for (const id of collectIdsToRemove(handle)) {
13759 unregisterWindowSlot(id);
13760 }
13761 unregisterWindowSlotsByOwner(handle);
13762 loadedHandles.delete(handle);
13763 }
13764 for (const entry of scripts) {
13765 if (!entry.handle || loadedHandles.has(entry.handle)) {
13766 continue;
13767 }
13768 await ensureScript(entry);
13769 }
13770 prevIdsByHandle = idsByHandleFrom(slots);
13771 };
13772 }
13773 const KEY_PREFIX = "desktop-mode-notice-dismissed";
13774 function currentUserSuffix() {
13775 const w = window.wp;
13776 const uid = w?.desktop?.config?.currentUserId;
13777 if (typeof uid === "number" && uid > 0) {
13778 return String(uid);
13779 }
13780 return "anon";
13781 }
13782 function storageKey() {
13783 return `${KEY_PREFIX}:${currentUserSuffix()}`;
13784 }
13785 function readMap() {
13786 try {
13787 const raw = window.localStorage.getItem(storageKey());
13788 if (!raw) {
13789 return {};
13790 }
13791 const parsed = JSON.parse(raw);
13792 if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
13793 return parsed;
13794 }
13795 } catch {
13796 }
13797 return {};
13798 }
13799 function writeMap(map) {
13800 try {
13801 window.localStorage.setItem(storageKey(), JSON.stringify(map));
13802 } catch {
13803 }
13804 }
13805 function isNoticeDismissed(id) {
13806 if (!id) {
13807 return false;
13808 }
13809 return readMap()[id] === true;
13810 }
13811 function markNoticeDismissed(id) {
13812 if (!id) {
13813 return;
13814 }
13815 const map = readMap();
13816 map[id] = true;
13817 writeMap(map);
13818 }
13819 function clearNoticeDismissed(id) {
13820 if (!id) {
13821 return;
13822 }
13823 const map = readMap();
13824 if (map[id]) {
13825 delete map[id];
13826 writeMap(map);
13827 }
13828 }
13829 const styles$5 = css`:host{display:flex;align-items:flex-start;gap:10px;width:100%;box-sizing:border-box;padding:10px 14px;font:var( --wpd-notice-font,13px/1.5 var( --desktop-mode-font,system-ui ) );color:var( --wpd-notice-color,var( --desktop-mode-text,#1d2327 ) );background:var( --wpd-notice-bg,rgba( 0,0,0,0.04 ) );border-block-end:1px solid var( --wpd-notice-border,rgba( 0,0,0,0.08 ) );border-inline-start:4px solid var( --wpd-notice-accent,#646970 )}:host( [ hidden ] ){display:none}.wpd-notice__icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;color:var( --wpd-notice-accent,#646970 )}.wpd-notice__icon[ hidden ]{display:none}.wpd-notice__label{flex:1;min-width:0;word-wrap:break-word}::slotted( a ){color:var( --wpd-notice-link,var( --wp-admin-theme-color,#2271b1 ) )}::slotted( p:first-child ){margin-block-start:0}::slotted( p:last-child ){margin-block-end:0}.wpd-notice__close{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;background:transparent;color:inherit;opacity:0.6;cursor:pointer;border-radius:4px;transition:opacity 0.12s ease,background-color 0.12s ease}.wpd-notice__close:hover{opacity:1;background:rgba( 0,0,0,0.06 )}.wpd-notice__close:focus-visible{opacity:1;outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:1px}.wpd-notice__close[ hidden ]{display:none}.wpd-notice__close svg{width:14px;height:14px}:host( [ tone='info' ] ){--wpd-notice-accent:var( --wpd-notice-info,#0969da );--wpd-notice-bg:var( --wpd-notice-info-bg,rgba( 9,105,218,0.08 ) );--wpd-notice-border:var( --wpd-notice-info-border,rgba( 9,105,218,0.16 ) )}:host( [ tone='success' ] ){--wpd-notice-accent:var( --wpd-notice-success,#1a7f37 );--wpd-notice-bg:var( --wpd-notice-success-bg,rgba( 26,127,55,0.08 ) );--wpd-notice-border:var( --wpd-notice-success-border,rgba( 26,127,55,0.16 ) )}:host( [ tone='warning' ] ){--wpd-notice-accent:var( --wpd-notice-warning,#9a6700 );--wpd-notice-bg:var( --wpd-notice-warning-bg,rgba( 154,103,0,0.08 ) );--wpd-notice-border:var( --wpd-notice-warning-border,rgba( 154,103,0,0.16 ) )}:host( [ tone='error' ] ),:host( [ tone='danger' ] ){--wpd-notice-accent:var( --wpd-notice-error,#cf222e );--wpd-notice-bg:var( --wpd-notice-error-bg,rgba( 207,34,46,0.08 ) );--wpd-notice-border:var( --wpd-notice-error-border,rgba( 207,34,46,0.16 ) )}:host( [ tone='neutral' ] ){--wpd-notice-accent:var( --wpd-notice-neutral,#57606a );--wpd-notice-bg:var( --wpd-notice-neutral-bg,rgba( 87,96,106,0.08 ) );--wpd-notice-border:var( --wpd-notice-neutral-border,rgba( 87,96,106,0.16 ) )}`;
13830 const _WpdNotice = class _WpdNotice extends Component {
13831 connectedCallback() {
13832 super.connectedCallback();
13833 if (!this.hasAttribute("role")) {
13834 this.setAttribute("role", "status");
13835 }
13836 if (!this.hasAttribute("tone")) {
13837 this.setAttribute("tone", "info");
13838 }
13839 const id = this.getAttribute("notice-id");
13840 if (id && isNoticeDismissed(id)) {
13841 this.hidden = true;
13842 }
13843 }
13844 /**
13845 * Imperatively dismiss the notice — hides the host and records
13846 * the dismissal in localStorage when `notice-id` is set.
13847 */
13848 dismiss() {
13849 this.hidden = true;
13850 const id = this.getAttribute("notice-id");
13851 if (id) {
13852 markNoticeDismissed(id);
13853 }
13854 this.emit("wpd-notice-dismiss", { noticeId: id ?? void 0 });
13855 }
13856 /**
13857 * Clear a previously recorded dismissal and re-show the notice.
13858 * Useful in tests and for "Show again" affordances.
13859 */
13860 undismiss() {
13861 const id = this.getAttribute("notice-id");
13862 if (id) {
13863 clearNoticeDismissed(id);
13864 }
13865 this.hidden = false;
13866 }
13867 render() {
13868 const icon = this.getAttribute("icon");
13869 const dismissible = !this.hasAttribute("not-dismissible");
13870 return html`
13871 <span
13872 class="wpd-notice__icon dashicons ${icon ?? ""}"
13873 ?hidden=${!icon}
13874 aria-hidden="true"
13875 ></span>
13876 <span class="wpd-notice__label"><slot></slot></span>
13877 <button
13878 type="button"
13879 class="wpd-notice__close"
13880 ?hidden=${!dismissible}
13881 aria-label=${__("Dismiss notice")}
13882 @click=${(e) => this._onDismiss(e)}
13883 >
13884 <svg viewBox="0 0 14 14" aria-hidden="true">
13885 <path
13886 d="M3 3 L11 11 M11 3 L3 11"
13887 stroke="currentColor"
13888 stroke-width="1.6"
13889 stroke-linecap="round"
13890 fill="none"
13891 ></path>
13892 </svg>
13893 </button>
13894 `;
13895 }
13896 _onDismiss(e) {
13897 e.preventDefault();
13898 e.stopPropagation();
13899 this.dismiss();
13900 }
13901 };
13902 _WpdNotice.props = ["tone", "notDismissible", "icon", "noticeId"];
13903 _WpdNotice.styles = [styles$5];
13904 _WpdNotice.help = {
13905 title: "Notice",
13906 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.",
13907 status: "experimental",
13908 since: "0.8.6",
13909 props: [
13910 {
13911 name: "tone",
13912 type: '"info" | "success" | "warning" | "error" | "danger" | "neutral"',
13913 description: "Color palette. Defaults to `info`. `error` and `danger` are aliases."
13914 },
13915 {
13916 name: "not-dismissible",
13917 type: "boolean",
13918 description: "Suppress the trailing close button. Defaults to dismissible."
13919 },
13920 {
13921 name: "icon",
13922 type: "string",
13923 description: "Optional Dashicons class for a leading glyph (e.g. `dashicons-info`)."
13924 },
13925 {
13926 name: "notice-id",
13927 type: "string",
13928 description: "Persistence key. When set, the notice records its dismissed state in localStorage so it stays closed across reloads for the same user."
13929 }
13930 ],
13931 slots: [
13932 {
13933 name: "(default)",
13934 description: "Message HTML. Links, `<strong>`, `<em>`, and other inline formatting are allowed."
13935 }
13936 ],
13937 events: [
13938 {
13939 name: "wpd-notice-dismiss",
13940 description: "Fires after the user clicks the close button.",
13941 detail: "{ noticeId?: string }"
13942 }
13943 ],
13944 cssProps: [
13945 { name: "--wpd-notice-bg", description: "Background color." },
13946 { name: "--wpd-notice-accent", description: "Left-edge stripe + icon color." },
13947 { name: "--wpd-notice-color", description: "Text color." },
13948 { name: "--wpd-notice-border", description: "Bottom border color." },
13949 { name: "--wpd-notice-link", description: "Color for slotted <a> elements." }
13950 ],
13951 example: html`
13952 <wpd-notice tone="warning" notice-id="docs/example">
13953 Heads up — this is a demo notice.
13954 <a href="#">Learn more</a>.
13955 </wpd-notice>
13956 `
13957 };
13958 let WpdNotice = _WpdNotice;
13959 defineComponent("wpd-notice", WpdNotice);
13960 const store$5 = createSharedStore(
13961 "desktop-mode/window-notices",
13962 () => ({ entries: /* @__PURE__ */ new Map() })
13963 );
13964 const ID_PATTERN = /^[a-z0-9_/-]+$/;
13965 function slotIdFor(id) {
13966 return `desktop-mode-notice/${id.toLowerCase()}`;
13967 }
13968 function buildNoticeElement(entry) {
13969 const el = document.createElement("wpd-notice");
13970 el.setAttribute("tone", entry.tone ?? "info");
13971 el.setAttribute("notice-id", entry.id);
13972 if (entry.dismissible === false) {
13973 el.setAttribute("not-dismissible", "");
13974 }
13975 if (entry.icon) {
13976 el.setAttribute("icon", entry.icon);
13977 }
13978 el.innerHTML = entry.message;
13979 return el;
13980 }
13981 function registerWindowNotice(entry) {
13982 if (!entry || typeof entry !== "object") {
13983 return () => {
13984 };
13985 }
13986 const id = String(entry.id ?? "").trim().toLowerCase();
13987 if (!id || !ID_PATTERN.test(id)) {
13988 return () => {
13989 };
13990 }
13991 if (typeof entry.message !== "string" || entry.message === "") {
13992 return () => {
13993 };
13994 }
13995 const normalised = { ...entry, id };
13996 store$5.state.entries.set(id, normalised);
13997 const slotId = slotIdFor(id);
13998 registerWindowSlot({
13999 id: slotId,
14000 slot: "after-titlebar",
14001 order: normalised.order ?? 100,
14002 // Append rather than clear — every notice slot entry appends
14003 // its own `<wpd-notice>` so multiple notices stack.
14004 replace: false,
14005 owner: normalised.owner,
14006 match: (win) => {
14007 const def = store$5.state.entries.get(id);
14008 if (!def) {
14009 return false;
14010 }
14011 if (typeof def.match !== "function") {
14012 return true;
14013 }
14014 try {
14015 return def.match(win) === true;
14016 } catch {
14017 return false;
14018 }
14019 },
14020 render: (host) => {
14021 const def = store$5.state.entries.get(id);
14022 if (!def) {
14023 return;
14024 }
14025 host.appendChild(buildNoticeElement(def));
14026 }
14027 });
14028 return () => unregisterWindowNotice(id);
14029 }
14030 function unregisterWindowNotice(id) {
14031 const key = String(id ?? "").trim().toLowerCase();
14032 if (!key) {
14033 return;
14034 }
14035 if (store$5.state.entries.delete(key)) {
14036 unregisterWindowSlot(slotIdFor(key));
14037 }
14038 }
14039 function listWindowNotices() {
14040 return Array.from(store$5.state.entries.values()).sort((a, b) => {
14041 const oa = a.order ?? 100;
14042 const ob = b.order ?? 100;
14043 if (oa !== ob) {
14044 return oa - ob;
14045 }
14046 return a.id.localeCompare(b.id);
14047 });
14048 }
14049 function dismissWindowNotice(id) {
14050 const key = String(id ?? "").trim().toLowerCase();
14051 if (!key) {
14052 return;
14053 }
14054 markNoticeDismissed(key);
14055 }
14056 function undismissWindowNotice(id) {
14057 const key = String(id ?? "").trim().toLowerCase();
14058 if (!key) {
14059 return;
14060 }
14061 clearNoticeDismissed(key);
14062 }
14063 function buildMatcher(match) {
14064 if (!match) {
14065 return void 0;
14066 }
14067 const ids = /* @__PURE__ */ new Set();
14068 if (typeof match.window === "string" && match.window !== "") {
14069 ids.add(match.window);
14070 }
14071 if (Array.isArray(match.windows)) {
14072 for (const id of match.windows) {
14073 if (typeof id === "string" && id !== "") {
14074 ids.add(id);
14075 }
14076 }
14077 }
14078 const needle = typeof match.urlContains === "string" && match.urlContains !== "" ? match.urlContains.toLowerCase() : null;
14079 if (ids.size === 0 && needle === null) {
14080 return void 0;
14081 }
14082 return (w) => {
14083 if (ids.size > 0 && !ids.has(w.id)) {
14084 return false;
14085 }
14086 if (needle !== null) {
14087 const url = typeof w.config.url === "string" ? w.config.url.toLowerCase() : "";
14088 if (!url.includes(needle)) {
14089 return false;
14090 }
14091 }
14092 return true;
14093 };
14094 }
14095 function applyServerWindowNotices(entries) {
14096 const wanted = /* @__PURE__ */ new Set();
14097 for (const entry of entries) {
14098 if (!entry || typeof entry.id !== "string" || !entry.id) {
14099 continue;
14100 }
14101 wanted.add(entry.id.toLowerCase());
14102 registerWindowNotice({
14103 id: entry.id,
14104 message: entry.message,
14105 tone: entry.tone,
14106 dismissible: entry.dismissible !== false,
14107 icon: entry.icon,
14108 match: buildMatcher(entry.match),
14109 order: typeof entry.order === "number" ? entry.order : void 0,
14110 // `owner` tag marks every server-shipped notice so a
14111 // targeted cleanup is trivial if/when we surface a sweep
14112 // helper later. Matches the convention used by the
14113 // command / settings-tab sync modules.
14114 owner: "__server__"
14115 });
14116 }
14117 for (const existing of listWindowNotices()) {
14118 if (existing.owner !== "__server__") {
14119 continue;
14120 }
14121 if (!wanted.has(existing.id)) {
14122 unregisterWindowNotice(existing.id);
14123 }
14124 }
14125 }
14126 const store$4 = createSharedStore(
14127 "desktop-mode/window-chrome-registry",
14128 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
14129 );
14130 const registry = store$4.state.registry;
14131 const listeners$3 = store$4.state.listeners;
14132 const WINDOW_CHROME_ID = /^[a-z0-9_/-]+$/;
14133 function registerWindowChrome(def) {
14134 const errors = [];
14135 if (!def || typeof def !== "object") {
14136 errors.push("def (not an object)");
14137 } else {
14138 if (typeof def.id !== "string" || def.id.trim() === "") {
14139 errors.push("id (missing)");
14140 } else if (!WINDOW_CHROME_ID.test(def.id.trim().toLowerCase())) {
14141 errors.push(
14142 `id (must match ${WINDOW_CHROME_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
14143 );
14144 }
14145 if (typeof def.match !== "function") {
14146 errors.push("match (must be a function)");
14147 }
14148 if (typeof def.render !== "function") {
14149 errors.push("render (must be a function)");
14150 }
14151 }
14152 throwOnRegistrationErrors("WindowChrome", errors, def);
14153 const id = def.id.trim().toLowerCase();
14154 registry.set(id, { ...def, id });
14155 notify$3();
14156 }
14157 function unregisterWindowChrome(id) {
14158 if (registry.delete(id.toLowerCase())) {
14159 notify$3();
14160 }
14161 }
14162 function unregisterWindowChromesByOwner(owner) {
14163 if (!owner) {
14164 return 0;
14165 }
14166 let removed = 0;
14167 for (const [id, def] of Array.from(registry.entries())) {
14168 if (def.owner === owner) {
14169 registry.delete(id);
14170 removed++;
14171 }
14172 }
14173 if (removed > 0) {
14174 notify$3();
14175 }
14176 return removed;
14177 }
14178 function listWindowChromes() {
14179 return Array.from(registry.values()).sort(
14180 (a, b) => a.id.localeCompare(b.id)
14181 );
14182 }
14183 function notify$3() {
14184 const snapshot = Array.from(listeners$3);
14185 for (const cb of snapshot) {
14186 try {
14187 cb();
14188 } catch (err) {
14189 if (typeof console !== "undefined") {
14190 console.error(
14191 "[desktop-mode] window-chrome registry listener threw:",
14192 err
14193 );
14194 }
14195 }
14196 }
14197 }
14198 function createWindowChromeRegistrySync() {
14199 const loadedHandles = /* @__PURE__ */ new Set();
14200 const loadedUrls = /* @__PURE__ */ new Set();
14201 let prevIdsByHandle = /* @__PURE__ */ new Map();
14202 const ensureScript = async (entry) => {
14203 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
14204 loadedHandles.add(entry.handle);
14205 return;
14206 }
14207 try {
14208 await loadVendorScript(entry.scriptUrl, {
14209 translations: entry.scriptTranslations,
14210 l10n: entry.scriptL10n,
14211 before: entry.scriptBefore,
14212 after: entry.scriptAfter
14213 });
14214 } catch (err) {
14215 doAction(HOOKS.SHELL_ERROR, {
14216 scope: "window-chrome-script-load",
14217 handle: entry.handle,
14218 url: entry.scriptUrl,
14219 error: err
14220 });
14221 return;
14222 }
14223 loadedUrls.add(entry.scriptUrl);
14224 loadedHandles.add(entry.handle);
14225 };
14226 const idsByHandleFrom = (chromes) => {
14227 const map = /* @__PURE__ */ new Map();
14228 if (!chromes) {
14229 return map;
14230 }
14231 for (const entry of chromes) {
14232 if (!entry.scriptHandle || !entry.id) {
14233 continue;
14234 }
14235 let set = map.get(entry.scriptHandle);
14236 if (!set) {
14237 set = /* @__PURE__ */ new Set();
14238 map.set(entry.scriptHandle, set);
14239 }
14240 set.add(entry.id);
14241 }
14242 return map;
14243 };
14244 const collectIdsToRemove = (handle) => {
14245 const ids = /* @__PURE__ */ new Set();
14246 for (const def of listWindowChromes()) {
14247 if (def.owner === handle) {
14248 ids.add(def.id);
14249 }
14250 }
14251 const declared = prevIdsByHandle.get(handle);
14252 if (declared) {
14253 for (const id of declared) {
14254 ids.add(id);
14255 }
14256 }
14257 return ids;
14258 };
14259 return async (scripts, chromes) => {
14260 const incomingHandles = /* @__PURE__ */ new Set();
14261 for (const entry of scripts) {
14262 if (entry.handle) {
14263 incomingHandles.add(entry.handle);
14264 }
14265 }
14266 for (const handle of Array.from(loadedHandles)) {
14267 if (incomingHandles.has(handle)) {
14268 continue;
14269 }
14270 for (const id of collectIdsToRemove(handle)) {
14271 unregisterWindowChrome(id);
14272 }
14273 unregisterWindowChromesByOwner(handle);
14274 loadedHandles.delete(handle);
14275 }
14276 for (const entry of scripts) {
14277 if (!entry.handle || loadedHandles.has(entry.handle)) {
14278 continue;
14279 }
14280 await ensureScript(entry);
14281 }
14282 prevIdsByHandle = idsByHandleFrom(chromes);
14283 };
14284 }
14285 const INITIAL_ORIGIN$2 = window.location.origin;
14286 let _connSeq = 0;
14287 const _connections = /* @__PURE__ */ new Map();
14288 const _connectionsByTarget = /* @__PURE__ */ new Map();
14289 const _syntheticIframes = /* @__PURE__ */ new Map();
14290 function registerSyntheticIframe(windowId, iframe) {
14291 _syntheticIframes.set(windowId, iframe);
14292 return () => {
14293 if (_syntheticIframes.get(windowId) === iframe) {
14294 _syntheticIframes.delete(windowId);
14295 }
14296 };
14297 }
14298 function nextId() {
14299 return `desktop-mode-conn-${++_connSeq}`;
14300 }
14301 function createConnectionBridge(manager2) {
14302 const sendToIframe = (win, message) => {
14303 try {
14304 win.contentWindow?.postMessage(message, INITIAL_ORIGIN$2);
14305 } catch (err) {
14306 if (typeof console !== "undefined") {
14307 console.error(
14308 "[desktop-mode] connection: postMessage failed",
14309 err
14310 );
14311 }
14312 }
14313 };
14314 const connect = (targetWindowId, opts = {}) => {
14315 const id = nextId();
14316 const topics = Array.isArray(opts.topics) ? [...opts.topics] : [];
14317 const subs = /* @__PURE__ */ new Map();
14318 const queue = [];
14319 let isOpen = false;
14320 let destroyed = false;
14321 const targetIframe = () => {
14322 const synth = _syntheticIframes.get(targetWindowId);
14323 if (synth) {
14324 return synth;
14325 }
14326 const w = manager2.getById(targetWindowId);
14327 return w?.iframe ?? null;
14328 };
14329 const isNativeTarget = () => {
14330 if (targetIframe()) {
14331 return false;
14332 }
14333 const w = manager2.getById(targetWindowId);
14334 return !!w && w.config?.native === true;
14335 };
14336 const nativeSubUnsubs = [];
14337 const flushQueue = () => {
14338 const iframe2 = targetIframe();
14339 if (!iframe2) {
14340 return;
14341 }
14342 while (queue.length) {
14343 const msg = queue.shift();
14344 sendToIframe(iframe2, {
14345 type: "desktop-mode-bridge-publish",
14346 connectionId: id,
14347 topic: msg.topic,
14348 payload: msg.payload
14349 });
14350 }
14351 };
14352 const conn = {
14353 id,
14354 target: targetWindowId,
14355 isOpen: () => isOpen,
14356 subscribe(topic, cb) {
14357 const wrapped = cb;
14358 if (isNativeTarget()) {
14359 const off = addParentSubscriber(
14360 targetWindowId,
14361 topic,
14362 (payload, meta) => {
14363 doAction(HOOKS.CONNECTION_MESSAGE, {
14364 connectionId: id,
14365 topic: meta.channel,
14366 direction: "in"
14367 });
14368 try {
14369 wrapped(payload, { topic: meta.channel });
14370 } catch (err) {
14371 if (typeof console !== "undefined") {
14372 console.error(
14373 "[desktop-mode] connection subscriber threw:",
14374 err
14375 );
14376 }
14377 }
14378 }
14379 );
14380 nativeSubUnsubs.push(off);
14381 return off;
14382 }
14383 let bucket22 = subs.get(topic);
14384 if (!bucket22) {
14385 bucket22 = /* @__PURE__ */ new Set();
14386 subs.set(topic, bucket22);
14387 }
14388 bucket22.add(wrapped);
14389 return () => {
14390 bucket22?.delete(wrapped);
14391 };
14392 },
14393 send(topic, payload) {
14394 if (destroyed) {
14395 return;
14396 }
14397 doAction(HOOKS.CONNECTION_MESSAGE, {
14398 connectionId: id,
14399 topic,
14400 direction: "out"
14401 });
14402 if (isNativeTarget()) {
14403 dispatchToNative(targetWindowId, topic, payload);
14404 return;
14405 }
14406 if (!isOpen) {
14407 queue.push({ topic, payload });
14408 return;
14409 }
14410 const iframe2 = targetIframe();
14411 if (!iframe2) {
14412 return;
14413 }
14414 sendToIframe(iframe2, {
14415 type: "desktop-mode-bridge-publish",
14416 connectionId: id,
14417 topic,
14418 payload
14419 });
14420 },
14421 disconnect() {
14422 conn._destroy("disconnect");
14423 },
14424 _targetWindow: targetIframe,
14425 _handleIframeMessage(data) {
14426 if (!data || typeof data !== "object") {
14427 return;
14428 }
14429 const msg = data;
14430 if (msg.type === "desktop-mode-bridge-handshake-ack") {
14431 if (isOpen) {
14432 return;
14433 }
14434 isOpen = true;
14435 doAction(HOOKS.CONNECTION_OPENED, {
14436 connectionId: id,
14437 targetWindowId,
14438 topics,
14439 // Ship the live Connection alongside the id so
14440 // iframe-initiated connections can be subscribed
14441 // to directly from the hook handler — without
14442 // `wp.desktop.getConnection(id)` plumbing the
14443 // payload would carry the id but no way to call
14444 // `.subscribe()` against it.
14445 connection: conn
14446 });
14447 try {
14448 opts.onOpen?.();
14449 } catch (err) {
14450 if (typeof console !== "undefined") {
14451 console.error(
14452 "[desktop-mode] connection.onOpen threw:",
14453 err
14454 );
14455 }
14456 }
14457 flushQueue();
14458 return;
14459 }
14460 if (msg.type === "desktop-mode-bridge-publish") {
14461 const m = data;
14462 const topic = typeof m.topic === "string" ? m.topic : "";
14463 if (!topic) {
14464 return;
14465 }
14466 doAction(HOOKS.CONNECTION_MESSAGE, {
14467 connectionId: id,
14468 topic,
14469 direction: "in"
14470 });
14471 const exact = subs.get(topic);
14472 if (exact) {
14473 for (const cb of Array.from(exact)) {
14474 try {
14475 cb(m.payload, { topic });
14476 } catch (err) {
14477 if (typeof console !== "undefined") {
14478 console.error(
14479 "[desktop-mode] connection subscriber threw:",
14480 err
14481 );
14482 }
14483 }
14484 }
14485 }
14486 const wildcard = subs.get("*");
14487 if (wildcard) {
14488 for (const cb of Array.from(wildcard)) {
14489 try {
14490 cb(m.payload, { topic });
14491 } catch (err) {
14492 if (typeof console !== "undefined") {
14493 console.error(
14494 "[desktop-mode] connection wildcard subscriber threw:",
14495 err
14496 );
14497 }
14498 }
14499 }
14500 }
14501 return;
14502 }
14503 if (msg.type === "desktop-mode-bridge-disconnect") {
14504 conn._destroy("disconnect");
14505 }
14506 },
14507 _destroy(reason) {
14508 if (destroyed) {
14509 return;
14510 }
14511 destroyed = true;
14512 const wasOpen = isOpen;
14513 isOpen = false;
14514 _connections.delete(id);
14515 const targetSet = _connectionsByTarget.get(targetWindowId);
14516 if (targetSet) {
14517 targetSet.delete(id);
14518 if (targetSet.size === 0) {
14519 _connectionsByTarget.delete(targetWindowId);
14520 }
14521 }
14522 for (const off of nativeSubUnsubs.splice(0)) {
14523 try {
14524 off();
14525 } catch {
14526 }
14527 }
14528 if (wasOpen) {
14529 const iframe2 = targetIframe();
14530 if (iframe2) {
14531 sendToIframe(iframe2, {
14532 type: "desktop-mode-bridge-disconnect",
14533 connectionId: id
14534 });
14535 }
14536 }
14537 doAction(HOOKS.CONNECTION_CLOSED, {
14538 connectionId: id,
14539 reason
14540 });
14541 try {
14542 opts.onClose?.(reason);
14543 } catch (err) {
14544 if (typeof console !== "undefined") {
14545 console.error(
14546 "[desktop-mode] connection.onClose threw:",
14547 err
14548 );
14549 }
14550 }
14551 }
14552 };
14553 _connections.set(id, conn);
14554 let bucket2 = _connectionsByTarget.get(targetWindowId);
14555 if (!bucket2) {
14556 bucket2 = /* @__PURE__ */ new Set();
14557 _connectionsByTarget.set(targetWindowId, bucket2);
14558 }
14559 bucket2.add(id);
14560 if (isNativeTarget()) {
14561 Promise.resolve().then(() => {
14562 if (destroyed || isOpen) {
14563 return;
14564 }
14565 isOpen = true;
14566 doAction(HOOKS.CONNECTION_OPENED, {
14567 connectionId: id,
14568 targetWindowId,
14569 topics
14570 });
14571 try {
14572 opts.onOpen?.();
14573 } catch (err) {
14574 if (typeof console !== "undefined") {
14575 console.error(
14576 "[desktop-mode] connection.onOpen threw:",
14577 err
14578 );
14579 }
14580 }
14581 });
14582 return conn;
14583 }
14584 const iframe = targetIframe();
14585 if (iframe) {
14586 sendToIframe(iframe, {
14587 type: "desktop-mode-bridge-handshake",
14588 connectionId: id,
14589 targetWindowId,
14590 topics
14591 });
14592 }
14593 return conn;
14594 };
14595 const routeIncomingFromIframe = (data, windowId) => {
14596 if (!data || typeof data !== "object") {
14597 return;
14598 }
14599 const msg = data;
14600 if (typeof msg.type !== "string" || !msg.type.startsWith("desktop-mode-bridge-")) {
14601 return;
14602 }
14603 if (msg.type === "desktop-mode-bridge-connection-request" && typeof msg.requestId === "string" && typeof windowId === "string" && windowId !== "") {
14604 handleConnectionRequest(windowId, msg.requestId, Array.isArray(msg.topics) ? msg.topics : []);
14605 return;
14606 }
14607 if (typeof msg.connectionId !== "string") {
14608 return;
14609 }
14610 const conn = _connections.get(msg.connectionId);
14611 conn?._handleIframeMessage(data);
14612 };
14613 const handleConnectionRequest = (windowId, requestId, topics) => {
14614 const synth = _syntheticIframes.get(windowId);
14615 const iframe = synth ?? manager2.getById(windowId)?.iframe ?? null;
14616 if (!iframe) {
14617 return;
14618 }
14619 const decision = applyFilters(
14620 HOOKS.IFRAME_CONNECTION_REQUEST,
14621 true,
14622 { windowId, requestId, topics: topics.slice() }
14623 );
14624 if (decision === false) {
14625 try {
14626 iframe.contentWindow?.postMessage({
14627 type: "desktop-mode-bridge-connection-ack",
14628 requestId,
14629 accepted: false,
14630 reason: "rejected"
14631 }, INITIAL_ORIGIN$2);
14632 } catch {
14633 }
14634 return;
14635 }
14636 const finalTopics = decision && typeof decision === "object" && Array.isArray(decision.topics) ? decision.topics : topics;
14637 const conn = connect(windowId, { topics: finalTopics });
14638 try {
14639 iframe.contentWindow?.postMessage({
14640 type: "desktop-mode-bridge-connection-ack",
14641 requestId,
14642 accepted: true,
14643 connectionId: conn.id
14644 }, INITIAL_ORIGIN$2);
14645 } catch {
14646 }
14647 };
14648 const onIframeReady = (windowId) => {
14649 const bucket2 = _connectionsByTarget.get(windowId);
14650 if (!bucket2) {
14651 return;
14652 }
14653 for (const connId of Array.from(bucket2)) {
14654 const conn = _connections.get(connId);
14655 if (!conn || conn.isOpen()) {
14656 continue;
14657 }
14658 const iframe = conn._targetWindow();
14659 if (!iframe) {
14660 continue;
14661 }
14662 sendToIframe(iframe, {
14663 type: "desktop-mode-bridge-handshake",
14664 connectionId: conn.id,
14665 targetWindowId: conn.target,
14666 topics: []
14667 // already negotiated client-side; iframe re-uses
14668 });
14669 }
14670 };
14671 const onWindowClosed = (windowId) => {
14672 const bucket2 = _connectionsByTarget.get(windowId);
14673 if (!bucket2) {
14674 return;
14675 }
14676 for (const connId of Array.from(bucket2)) {
14677 const conn = _connections.get(connId);
14678 conn?._destroy("window-closed");
14679 }
14680 };
14681 const getConnection = (connectionId) => {
14682 const conn = _connections.get(connectionId);
14683 return conn ?? null;
14684 };
14685 return {
14686 connect,
14687 getConnection,
14688 routeIncomingFromIframe,
14689 onIframeReady,
14690 onWindowClosed
14691 };
14692 }
14693 const __vite_import_meta_env__ = {};
14694 function devLog(...args) {
14695 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;
14696 if (mode !== "production") {
14697 console.log(...args);
14698 }
14699 }
14700 const OWNER_PREFIX = "iframe:";
14701 function ownerFor(windowId) {
14702 return OWNER_PREFIX + windowId;
14703 }
14704 function iconFor(harvested) {
14705 if (harvested.icon && typeof harvested.icon === "string" && harvested.icon.startsWith("dashicons-")) {
14706 return harvested.icon;
14707 }
14708 return harvested.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
14709 }
14710 function slugFor(windowId, name) {
14711 const safeName = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
14712 const safeWin = windowId.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
14713 return `win-${safeWin}-${safeName}`;
14714 }
14715 class IframeCommandBridge {
14716 constructor(opts) {
14717 this.subscribedWindowId = null;
14718 this.manager = opts.manager;
14719 this.adminUrl = opts.adminUrl;
14720 }
14721 /** Wire up the focus / close / message listeners. Idempotent. */
14722 install() {
14723 document.addEventListener("desktop-mode-window-focused", (e) => {
14724 const detail = e.detail;
14725 if (detail && typeof detail.windowId === "string") {
14726 this.onFocused(detail.windowId);
14727 }
14728 });
14729 document.addEventListener("desktop-mode-window-closed", (e) => {
14730 const detail = e.detail;
14731 if (detail && typeof detail.windowId === "string") {
14732 unregisterByOwner(ownerFor(detail.windowId));
14733 if (this.subscribedWindowId === detail.windowId) {
14734 this.subscribedWindowId = null;
14735 }
14736 }
14737 });
14738 document.addEventListener("desktop-mode-window-changed", (e) => {
14739 const detail = e.detail;
14740 if (!detail || typeof detail.windowId !== "string") {
14741 return;
14742 }
14743 if (detail.reason !== "state") {
14744 return;
14745 }
14746 if (detail.state !== "minimized") {
14747 return;
14748 }
14749 if (this.subscribedWindowId === detail.windowId) {
14750 this.subscribedWindowId = null;
14751 }
14752 });
14753 window.addEventListener("message", (e) => {
14754 if (e.origin !== window.location.origin) {
14755 return;
14756 }
14757 const data = e.data;
14758 if (!data || typeof data.type !== "string") {
14759 return;
14760 }
14761 if (data.type === "desktop-mode-bridge-ready") {
14762 const win2 = this.manager.findByIframeSource(e.source);
14763 if (win2 && win2.id === this.subscribedWindowId) {
14764 this.sendSubscribe(win2.id);
14765 }
14766 return;
14767 }
14768 if (data.type !== "desktop-mode-commands-list") {
14769 return;
14770 }
14771 if (!Array.isArray(data.commands)) {
14772 return;
14773 }
14774 const win = this.manager.findByIframeSource(e.source);
14775 if (!win) {
14776 return;
14777 }
14778 if (win.id !== this.subscribedWindowId) {
14779 return;
14780 }
14781 this.applyList(win.id, data.commands);
14782 });
14783 const focused = this.manager.getFocused();
14784 if (focused) {
14785 this.onFocused(focused.id);
14786 }
14787 }
14788 onFocused(windowId) {
14789 if (this.subscribedWindowId === windowId) {
14790 return;
14791 }
14792 if (this.subscribedWindowId) {
14793 const prev = this.manager.getById(this.subscribedWindowId);
14794 if (prev && prev.iframe && prev.iframe.contentWindow) {
14795 try {
14796 prev.iframe.contentWindow.postMessage(
14797 { type: "desktop-mode-commands-unsubscribe" },
14798 window.location.origin
14799 );
14800 } catch {
14801 }
14802 }
14803 unregisterByOwner(ownerFor(this.subscribedWindowId));
14804 }
14805 this.subscribedWindowId = windowId;
14806 this.sendSubscribe(windowId);
14807 }
14808 sendSubscribe(windowId) {
14809 const win = this.manager.getById(windowId);
14810 if (!win) {
14811 return;
14812 }
14813 if (!win.iframe) {
14814 return;
14815 }
14816 if (!win.iframe.contentWindow) {
14817 return;
14818 }
14819 try {
14820 win.iframe.contentWindow.postMessage(
14821 { type: "desktop-mode-commands-subscribe" },
14822 window.location.origin
14823 );
14824 } catch (err) {
14825 devLog("[wpd-cmd:parent] sendSubscribe: postMessage threw", err);
14826 }
14827 }
14828 applyList(windowId, commands) {
14829 const owner = ownerFor(windowId);
14830 unregisterByOwner(owner);
14831 for (const cmd of commands) {
14832 if (!cmd || !cmd.name || !cmd.label) {
14833 continue;
14834 }
14835 const slug = slugFor(windowId, cmd.name);
14836 const safeSvg = typeof cmd.iconSvg === "string" && cmd.iconSvg !== "" ? sanitizeIconSvg(cmd.iconSvg) : "";
14837 const def = {
14838 slug,
14839 label: cmd.label,
14840 icon: iconFor(cmd),
14841 iconSvg: safeSvg !== "" ? safeSvg : void 0,
14842 owner,
14843 // Harvested commands are contextual by construction —
14844 // they come from whichever window has focus. Surface
14845 // them eagerly so the user sees "Duplicate block" /
14846 // "Toggle distraction free" without having to type `/`
14847 // first.
14848 eager: true,
14849 run: cmd.kind === "navigate" && cmd.url ? this.runNavigate(cmd.url, cmd.label, iconFor(cmd)) : this.runProxy(windowId, cmd.name)
14850 };
14851 try {
14852 registerCommand(def);
14853 } catch (err) {
14854 console.error(
14855 "[desktop-mode] iframe-bridge: dropping bad command",
14856 def,
14857 err
14858 );
14859 }
14860 }
14861 }
14862 runNavigate(url, title, icon) {
14863 return (_args, ctx) => {
14864 ctx.close();
14865 if (tryNativeUrlRemap(url)) {
14866 return;
14867 }
14868 const id = deriveWindowId(url, this.adminUrl);
14869 this.manager.open({ id, baseId: id, url, title, icon });
14870 };
14871 }
14872 runProxy(windowId, name) {
14873 return (_args, ctx) => {
14874 ctx.close();
14875 const win = this.manager.getById(windowId);
14876 if (!win || !win.iframe || !win.iframe.contentWindow) {
14877 return;
14878 }
14879 try {
14880 win.iframe.contentWindow.postMessage(
14881 { type: "desktop-mode-commands-invoke", name },
14882 window.location.origin
14883 );
14884 } catch {
14885 }
14886 this.manager.focus(win);
14887 };
14888 }
14889 }
14890 const OWNER = "global";
14891 const NAV_HREF_LITERAL_RE = /(?:document\.location\.href|window\.location\.href|location\.href)\s*=\s*['"]([^'"$]+?)['"]/;
14892 const NAV_ASSIGN_LITERAL_RE = /(?:document\.location|window\.location|location)\s*=\s*['"]([^'"$]+?)['"]/;
14893 const NAV_CALL_LITERAL_RE = /location\.(?:assign|replace)\s*\(\s*['"]([^'"$]+?)['"]\s*\)/;
14894 const NAV_INTENT_RE = /(?:document\.location|window\.location|location)\s*(?:\.href\s*)?=|location\.(?:assign|replace)\s*\(/;
14895 const SITE_EDITOR_INTENT_RE = /getSiteEditorPage\s*\(|site-editor\.php/;
14896 const SITE_EDITOR_NAME_RE = /^(wp_template_part|wp_template|wp_navigation|wp_block)-(.+)$/;
14897 function lookupMenuCommand(name) {
14898 const list2 = window.__desktopModeMenuCommands;
14899 if (!Array.isArray(list2)) {
14900 return null;
14901 }
14902 for (const entry of list2) {
14903 if (entry && typeof entry === "object" && entry.name === name && typeof entry.url === "string" && entry.url !== "") {
14904 return {
14905 label: typeof entry.label === "string" ? entry.label : "",
14906 url: entry.url
14907 };
14908 }
14909 }
14910 return null;
14911 }
14912 class ShellCommandHarvester {
14913 constructor(opts) {
14914 this.mounted = false;
14915 this.host = null;
14916 this.root = null;
14917 this.kindCache = /* @__PURE__ */ Object.create(null);
14918 this.callbackCache = /* @__PURE__ */ Object.create(null);
14919 this.lastFingerprint = "";
14920 this.manager = opts.manager;
14921 this.adminUrl = opts.adminUrl;
14922 }
14923 /** Mount the harvester. Idempotent. Safe to call before `wp.data` loads. */
14924 install() {
14925 this.tryMount(0);
14926 }
14927 tryMount(attempt) {
14928 if (this.mounted) {
14929 return;
14930 }
14931 const wp = window.wp;
14932 if (!wp || !wp.data || !wp.element || typeof wp.data.subscribe !== "function") {
14933 if (attempt < 40) {
14934 window.setTimeout(() => this.tryMount(attempt + 1), 150);
14935 }
14936 return;
14937 }
14938 this.mount();
14939 }
14940 mount() {
14941 const wp = window.wp;
14942 const el = wp.element;
14943 const data = wp.data;
14944 const createEl = el.createElement;
14945 const useEffect = el.useEffect;
14946 const useRef = el.useRef;
14947 const useMemo = el.useMemo;
14948 const useSelect = data.useSelect;
14949 if (typeof createEl !== "function" || typeof useEffect !== "function" || typeof useRef !== "function" || typeof useMemo !== "function" || typeof useSelect !== "function" || typeof el.createRoot !== "function") {
14950 return;
14951 }
14952 this.mounted = true;
14953 const host = document.createElement("div");
14954 host.setAttribute("aria-hidden", "true");
14955 host.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;pointer-events:none;left:-9999px;top:-9999px;";
14956 (document.body || document.documentElement).appendChild(host);
14957 this.host = host;
14958 const bucket2 = {
14959 perLoader: {},
14960 statics: [],
14961 loadersList: []
14962 };
14963 const fingerprint2 = (cmds) => {
14964 if (!Array.isArray(cmds) || cmds.length === 0) {
14965 return "";
14966 }
14967 const keys = new Array(cmds.length);
14968 for (let i = 0; i < cmds.length; i++) {
14969 const c = cmds[i];
14970 keys[i] = c && c.name ? c.name : "";
14971 }
14972 return keys.join("|");
14973 };
14974 const mergeAndPublish = () => {
14975 let merged = [];
14976 for (const name of bucket2.loadersList) {
14977 const slice = bucket2.perLoader[name];
14978 if (Array.isArray(slice)) {
14979 merged = merged.concat(slice);
14980 }
14981 }
14982 if (Array.isArray(bucket2.statics)) {
14983 merged = merged.concat(bucket2.statics);
14984 }
14985 this.callbackCache = /* @__PURE__ */ Object.create(null);
14986 for (const cc of merged) {
14987 if (cc && cc.name && typeof cc.callback === "function") {
14988 this.callbackCache[cc.name] = cc.callback;
14989 }
14990 }
14991 this.publish(merged);
14992 };
14993 const LoaderSlot = (props) => {
14994 const loader = props.loader;
14995 let result = null;
14996 try {
14997 result = loader.hook({ search: "" });
14998 } catch {
14999 }
15000 const cmds = result && Array.isArray(result.commands) ? result.commands : [];
15001 const key = useMemo(() => fingerprint2(cmds), [cmds]);
15002 useEffect(() => {
15003 bucket2.perLoader[loader.name] = cmds;
15004 mergeAndPublish();
15005 }, [key]);
15006 useEffect(() => {
15007 return () => {
15008 delete bucket2.perLoader[loader.name];
15009 mergeAndPublish();
15010 };
15011 }, []);
15012 return null;
15013 };
15014 const Harvester = () => {
15015 const loaders = useSelect((s) => {
15016 const ss = s("core/commands");
15017 if (!ss || typeof ss.getCommandLoaders !== "function") {
15018 return [];
15019 }
15020 return [
15021 ...ss.getCommandLoaders(false) || [],
15022 ...ss.getCommandLoaders(true) || []
15023 ];
15024 }, []);
15025 const staticCmds = useSelect((s) => {
15026 const ss = s("core/commands");
15027 if (!ss || typeof ss.getCommands !== "function") {
15028 return [];
15029 }
15030 return [
15031 ...ss.getCommands(false) || [],
15032 ...ss.getCommands(true) || []
15033 ];
15034 }, []);
15035 const loadersNames = useMemo(() => {
15036 return Array.isArray(loaders) ? loaders.map((l) => l ? l.name || "" : "") : [];
15037 }, [loaders]);
15038 const loadersKey = loadersNames.join("|");
15039 useEffect(() => {
15040 bucket2.loadersList = loadersNames;
15041 mergeAndPublish();
15042 }, [loadersKey]);
15043 const staticKey = useMemo(
15044 () => fingerprint2(Array.isArray(staticCmds) ? staticCmds : []),
15045 [staticCmds]
15046 );
15047 useEffect(() => {
15048 bucket2.statics = Array.isArray(staticCmds) ? staticCmds : [];
15049 mergeAndPublish();
15050 }, [staticKey]);
15051 if (!Array.isArray(loaders) || loaders.length === 0) {
15052 return null;
15053 }
15054 const children = [];
15055 for (const loader of loaders) {
15056 if (!loader || typeof loader.hook !== "function") {
15057 continue;
15058 }
15059 children.push(
15060 createEl(LoaderSlot, { key: loader.name, loader })
15061 );
15062 }
15063 return createEl(el.Fragment || "div", null, children);
15064 };
15065 try {
15066 this.root = el.createRoot(host);
15067 this.root.render(createEl(Harvester));
15068 } catch {
15069 this.mounted = false;
15070 this.root = null;
15071 if (this.host && this.host.parentNode) {
15072 this.host.parentNode.removeChild(this.host);
15073 }
15074 this.host = null;
15075 }
15076 }
15077 publish(raw) {
15078 const seen = /* @__PURE__ */ Object.create(null);
15079 const classified = [];
15080 for (const cmd of raw) {
15081 if (!cmd || !cmd.name || !cmd.label) {
15082 continue;
15083 }
15084 if (cmd.disabled) {
15085 continue;
15086 }
15087 if (seen[cmd.name]) {
15088 continue;
15089 }
15090 seen[cmd.name] = true;
15091 classified.push(this.classify(cmd));
15092 }
15093 let key = "";
15094 for (const c of classified) {
15095 key += `${c.name}|${c.kind}|${c.url || ""}
15096 `;
15097 }
15098 if (key === this.lastFingerprint) {
15099 return;
15100 }
15101 this.lastFingerprint = key;
15102 unregisterByOwner(OWNER);
15103 for (const c of classified) {
15104 if (c.kind === "skip") {
15105 continue;
15106 }
15107 const slug = `global-${c.name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-")}`;
15108 const icon = this.iconFor(c);
15109 const def = {
15110 slug,
15111 label: c.label,
15112 icon,
15113 iconSvg: c.iconSvg && c.iconSvg !== "" ? sanitizeIconSvg(c.iconSvg) : void 0,
15114 owner: OWNER,
15115 // NOT eager. The palette splits the registry into two
15116 // disjoint surfaces: `eager` commands show on empty
15117 // input (and are excluded from slash search at
15118 // `src/ai-assistant/impl.ts:494`); non-eager commands
15119 // show when the user types `/<query>`. The WP baseline
15120 // is large (~150 entries) and meant to be searched —
15121 // surfacing it eagerly would drown the iframe-harvested
15122 // contextual shortcuts on every open. Slash-search is
15123 // the right surface for it, matching the native WP
15124 // palette UX (open, type, find).
15125 run: c.kind === "navigate" && c.url ? this.runNavigate(c.url, c.windowTitle || c.label, icon) : this.runInvoke(c.name, c.label, icon)
15126 };
15127 try {
15128 registerCommand(def);
15129 } catch (err) {
15130 console.error(
15131 "[desktop-mode] shell-harvester: dropping bad command",
15132 def,
15133 err
15134 );
15135 }
15136 }
15137 }
15138 classify(cmd) {
15139 const out = {
15140 name: String(cmd.name),
15141 label: String(cmd.label),
15142 icon: typeof cmd.icon === "string" ? cmd.icon : void 0,
15143 iconSvg: void 0,
15144 kind: "action",
15145 url: void 0,
15146 callback: typeof cmd.callback === "function" ? cmd.callback : void 0
15147 };
15148 const cached = this.kindCache[out.name];
15149 if (cached) {
15150 out.kind = cached.kind;
15151 out.url = cached.url;
15152 out.iconSvg = cached.iconSvg;
15153 return out;
15154 }
15155 if (cmd.icon && typeof cmd.icon !== "string") {
15156 out.iconSvg = this.renderIcon(cmd.icon);
15157 }
15158 const menuEntry = lookupMenuCommand(out.name);
15159 if (menuEntry) {
15160 try {
15161 out.url = new URL(menuEntry.url, this.adminUrl).toString();
15162 out.kind = "navigate";
15163 if (menuEntry.label !== "") {
15164 out.windowTitle = menuEntry.label;
15165 }
15166 } catch {
15167 out.kind = "skip";
15168 }
15169 this.kindCache[out.name] = {
15170 kind: out.kind,
15171 url: out.url,
15172 iconSvg: out.iconSvg
15173 };
15174 return out;
15175 }
15176 if (typeof cmd.callback === "function") {
15177 let src = "";
15178 try {
15179 src = Function.prototype.toString.call(cmd.callback);
15180 } catch {
15181 src = "";
15182 }
15183 const literal = src.match(NAV_HREF_LITERAL_RE) || src.match(NAV_ASSIGN_LITERAL_RE) || src.match(NAV_CALL_LITERAL_RE);
15184 if (literal && literal[1]) {
15185 try {
15186 out.url = new URL(literal[1], window.location.href).toString();
15187 out.kind = "navigate";
15188 } catch {
15189 out.kind = "action";
15190 }
15191 } else if (NAV_INTENT_RE.test(src)) {
15192 const isSiteEditorIntent = SITE_EDITOR_INTENT_RE.test(src);
15193 const nameMatch = isSiteEditorIntent ? out.name.match(SITE_EDITOR_NAME_RE) : null;
15194 if (nameMatch) {
15195 const entityType = nameMatch[1];
15196 const entityId = nameMatch[2];
15197 const p = `/${entityType}/${entityId}`;
15198 try {
15199 const siteEditor = new URL("site-editor.php", this.adminUrl);
15200 siteEditor.searchParams.set("p", p);
15201 siteEditor.searchParams.set("canvas", "edit");
15202 out.url = siteEditor.toString();
15203 out.kind = "navigate";
15204 } catch {
15205 out.kind = "skip";
15206 }
15207 } else {
15208 out.kind = "skip";
15209 }
15210 }
15211 }
15212 this.kindCache[out.name] = {
15213 kind: out.kind,
15214 url: out.url,
15215 iconSvg: out.iconSvg
15216 };
15217 return out;
15218 }
15219 renderIcon(icon) {
15220 const wp = window.wp;
15221 if (!wp || !wp.element || typeof wp.element.renderToString !== "function") {
15222 return "";
15223 }
15224 try {
15225 const rendered = wp.element.renderToString(icon);
15226 if (typeof rendered === "string" && rendered.toLowerCase().startsWith("<svg")) {
15227 return rendered;
15228 }
15229 } catch {
15230 }
15231 return "";
15232 }
15233 iconFor(c) {
15234 if (c.icon && c.icon.startsWith("dashicons-")) {
15235 return c.icon;
15236 }
15237 return c.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
15238 }
15239 runNavigate(url, title, icon) {
15240 return (_args, ctx) => {
15241 ctx.close();
15242 if (tryNativeUrlRemap(url)) {
15243 return;
15244 }
15245 const id = deriveWindowId(url, this.adminUrl);
15246 this.manager.open({ id, baseId: id, url, title, icon });
15247 };
15248 }
15249 runInvoke(name, title, icon) {
15250 return (_args, ctx) => {
15251 const cb = this.callbackCache[name];
15252 if (typeof cb !== "function") {
15253 ctx.close();
15254 return;
15255 }
15256 const captured = this.runWithNavCapture(cb);
15257 if (captured) {
15258 ctx.close();
15259 const id = deriveWindowId(captured, this.adminUrl);
15260 this.manager.open({ id, baseId: id, url: captured, title, icon });
15261 }
15262 };
15263 }
15264 /**
15265 * Invoke `cb` with navigation sinks (`document.location`,
15266 * `window.location`, `location.assign`, `location.replace`)
15267 * shadowed so any assignment is captured instead of navigating
15268 * the shell. Returns the captured URL or `null` if the callback
15269 * was a pure JS action.
15270 *
15271 * The shadow uses `Object.defineProperty` on the document /
15272 * window instance to override the prototype's accessor for the
15273 * duration of the call. `delete` afterwards unshadows so the
15274 * native setter is restored.
15275 */
15276 runWithNavCapture(cb) {
15277 let captured = null;
15278 const setCaptured = (v) => {
15279 if (captured === null && typeof v === "string" && v !== "") {
15280 captured = v;
15281 }
15282 };
15283 const realLocation = window.location;
15284 const locationProxy = new Proxy(realLocation, {
15285 get(target2, prop2) {
15286 const value = target2[prop2];
15287 if (prop2 === "assign" || prop2 === "replace") {
15288 return (url) => setCaptured(url);
15289 }
15290 if (typeof value === "function") {
15291 return value.bind(target2);
15292 }
15293 return value;
15294 },
15295 set(_target, prop2, value) {
15296 if (prop2 === "href") {
15297 setCaptured(value);
15298 return true;
15299 }
15300 return true;
15301 }
15302 });
15303 const shadowed = [];
15304 const installShadow = (obj) => {
15305 try {
15306 Object.defineProperty(obj, "location", {
15307 configurable: true,
15308 get: () => locationProxy,
15309 set: (v) => setCaptured(v)
15310 });
15311 shadowed.push({ obj, key: "location" });
15312 } catch {
15313 }
15314 };
15315 installShadow(document);
15316 installShadow(window);
15317 try {
15318 cb({ close: () => {
15319 } });
15320 } catch {
15321 } finally {
15322 for (const s of shadowed) {
15323 try {
15324 delete s.obj[s.key];
15325 } catch {
15326 }
15327 }
15328 }
15329 return captured;
15330 }
15331 }
15332 const seed$2 = [];
15333 function register(def) {
15334 throwOnRegistrationErrors(
15335 "Widget",
15336 collectRegistrationErrors(def, WIDGET_CHECKS),
15337 def
15338 );
15339 const idx = seed$2.findIndex((w) => w.id === def.id);
15340 if (idx >= 0) {
15341 seed$2[idx] = def;
15342 } else {
15343 seed$2.push(def);
15344 }
15345 }
15346 function unregister(id) {
15347 const idx = seed$2.findIndex((w) => w.id === id);
15348 if (idx >= 0) {
15349 seed$2.splice(idx, 1);
15350 }
15351 }
15352 function all() {
15353 const copy = seed$2.slice();
15354 const filtered = applyFilters(HOOKS.WIDGETS, copy);
15355 if (!Array.isArray(filtered)) {
15356 if (typeof console !== "undefined") {
15357 console.warn(
15358 "[desktop-mode] `desktop-mode.widgets` filter returned a non-array; falling back to seed list."
15359 );
15360 }
15361 return copy;
15362 }
15363 return filtered.filter(isValidDef);
15364 }
15365 function get(id) {
15366 return all().find((w) => w.id === id);
15367 }
15368 const WIDGET_CHECKS = [
15369 {
15370 field: "id",
15371 message: "missing or not a non-empty string",
15372 valid: (d) => typeof d.id === "string" && d.id !== ""
15373 },
15374 {
15375 field: "label",
15376 message: "missing or not a non-empty string",
15377 valid: (d) => typeof d.label === "string" && d.label !== ""
15378 },
15379 {
15380 field: "description",
15381 message: "not a string",
15382 valid: (d) => typeof d.description === "string"
15383 },
15384 {
15385 field: "icon",
15386 message: "missing or not a non-empty string",
15387 valid: (d) => typeof d.icon === "string" && d.icon !== ""
15388 },
15389 {
15390 field: "mount",
15391 message: "not a function",
15392 valid: (d) => typeof d.mount === "function"
15393 }
15394 ];
15395 function isValidDef(def) {
15396 return collectRegistrationErrors(def, WIDGET_CHECKS).length === 0;
15397 }
15398 let active$2 = null;
15399 function openWidgetPicker(options) {
15400 if (active$2) {
15401 return;
15402 }
15403 const panel2 = document.createElement("div");
15404 panel2.className = "desktop-mode-widget-picker";
15405 panel2.setAttribute("role", "menu");
15406 panel2.setAttribute("aria-label", __("Add widget"));
15407 const title = document.createElement("div");
15408 title.className = "desktop-mode-widget-picker__title";
15409 title.textContent = __("Add widget");
15410 panel2.appendChild(title);
15411 const list2 = document.createElement("div");
15412 list2.className = "desktop-mode-widget-picker__list";
15413 panel2.appendChild(list2);
15414 paintList(list2, options);
15415 document.body.appendChild(panel2);
15416 positionPanel(panel2, options.anchor);
15417 const onOutsidePointerDown = (e) => {
15418 const target2 = e.target;
15419 if (!target2) {
15420 return;
15421 }
15422 if (panel2.contains(target2) || options.anchor.contains(target2)) {
15423 return;
15424 }
15425 closeWidgetPicker();
15426 };
15427 window.setTimeout(() => {
15428 document.addEventListener("pointerdown", onOutsidePointerDown, true);
15429 }, 0);
15430 const onKeyDown = (e) => {
15431 if (e.key === "Escape") {
15432 closeWidgetPicker();
15433 }
15434 };
15435 document.addEventListener("keydown", onKeyDown);
15436 active$2 = { panel: panel2, options, onOutsidePointerDown, onKeyDown };
15437 const first = list2.querySelector(
15438 "button:not([disabled])"
15439 );
15440 first?.focus();
15441 }
15442 function refreshWidgetPicker() {
15443 if (!active$2) {
15444 return;
15445 }
15446 const list2 = active$2.panel.querySelector(
15447 ".desktop-mode-widget-picker__list"
15448 );
15449 if (list2) {
15450 paintList(list2, active$2.options);
15451 }
15452 }
15453 function closeWidgetPicker() {
15454 if (!active$2) {
15455 return;
15456 }
15457 document.removeEventListener(
15458 "pointerdown",
15459 active$2.onOutsidePointerDown,
15460 true
15461 );
15462 document.removeEventListener("keydown", active$2.onKeyDown);
15463 active$2.panel.remove();
15464 active$2 = null;
15465 }
15466 function paintList(list2, options) {
15467 list2.innerHTML = "";
15468 const enabled = new Set(options.enabledIds());
15469 const defs = options.registry();
15470 if (defs.length === 0) {
15471 const empty = document.createElement("div");
15472 empty.className = "desktop-mode-widget-picker__empty";
15473 empty.textContent = __(
15474 "No widgets available. Activate a plugin that registers one, or see the docs for the registerWidget API."
15475 );
15476 list2.appendChild(empty);
15477 return;
15478 }
15479 for (const def of defs) {
15480 const entry = document.createElement("button");
15481 entry.type = "button";
15482 entry.className = "desktop-mode-widget-picker__entry";
15483 const isAdded = enabled.has(def.id);
15484 if (isAdded) {
15485 entry.classList.add(
15486 "desktop-mode-widget-picker__entry--added"
15487 );
15488 entry.disabled = true;
15489 entry.setAttribute("aria-disabled", "true");
15490 }
15491 entry.setAttribute("role", "menuitem");
15492 let ariaLabel;
15493 if (isAdded) {
15494 ariaLabel = sprintf(__("%s (already added)"), def.label);
15495 } else {
15496 ariaLabel = sprintf(__("Add %s"), def.label);
15497 }
15498 entry.setAttribute("aria-label", ariaLabel);
15499 const icon = document.createElement("span");
15500 icon.className = `desktop-mode-widget-picker__entry-icon dashicons ${def.icon}`;
15501 icon.setAttribute("aria-hidden", "true");
15502 entry.appendChild(icon);
15503 const textWrap = document.createElement("span");
15504 textWrap.className = "desktop-mode-widget-picker__entry-text";
15505 const label = document.createElement("span");
15506 label.className = "desktop-mode-widget-picker__entry-label";
15507 label.textContent = def.label;
15508 textWrap.appendChild(label);
15509 if (def.description) {
15510 const desc = document.createElement("span");
15511 desc.className = "desktop-mode-widget-picker__entry-description";
15512 desc.textContent = def.description;
15513 textWrap.appendChild(desc);
15514 }
15515 entry.appendChild(textWrap);
15516 if (isAdded) {
15517 const status = document.createElement("span");
15518 status.className = "desktop-mode-widget-picker__entry-status";
15519 status.textContent = __("Added");
15520 entry.appendChild(status);
15521 }
15522 if (!isAdded) {
15523 entry.addEventListener("click", (e) => {
15524 e.preventDefault();
15525 e.stopPropagation();
15526 options.onAdd(def.id);
15527 });
15528 }
15529 list2.appendChild(entry);
15530 }
15531 }
15532 function positionPanel(panel2, anchor) {
15533 const rect = anchor.getBoundingClientRect();
15534 panel2.style.position = "fixed";
15535 panel2.style.left = "0px";
15536 panel2.style.top = "0px";
15537 panel2.style.visibility = "hidden";
15538 const panelRect = panel2.getBoundingClientRect();
15539 const width = panelRect.width || 320;
15540 const height = panelRect.height || 200;
15541 const gap = 6;
15542 let left = rect.right - width;
15543 let top = rect.top - height - gap;
15544 if (left < 8) {
15545 left = 8;
15546 }
15547 if (top < 8) {
15548 top = rect.bottom + gap;
15549 }
15550 panel2.style.left = `${Math.round(left)}px`;
15551 panel2.style.top = `${Math.round(top)}px`;
15552 panel2.style.visibility = "";
15553 }
15554 const FLOATING_CLASS = "desktop-mode-widgets__card--floating";
15555 const MOVABLE_CLASS = "desktop-mode-widgets__card--movable";
15556 const RESIZABLE_CLASS = "desktop-mode-widgets__card--resizable";
15557 const DRAGGING_CLASS = "desktop-mode-widgets__card--dragging";
15558 const RESIZING_CLASS = "desktop-mode-widgets__card--resizing";
15559 const DEFAULT_MIN_WIDTH = 160;
15560 const DEFAULT_MIN_HEIGHT = 80;
15561 const DEFAULT_WIDTH$1 = 280;
15562 const DEFAULT_HEIGHT$1 = 180;
15563 const VIEWPORT_MARGIN = 20;
15564 const DRAG_THRESHOLD_PX$1 = 5;
15565 const DRAG_THRESHOLD_SQUARED = DRAG_THRESHOLD_PX$1 * DRAG_THRESHOLD_PX$1;
15566 const DRAG_EXCLUDED_SELECTORS = 'input, textarea, select, button, a, [contenteditable="true"]';
15567 function buildFrame(def, ctx, handlers2) {
15568 const card = document.createElement("div");
15569 card.className = "desktop-mode-widgets__card";
15570 card.dataset.widgetId = def.id;
15571 const movable = def.movable === true;
15572 const resizable = def.resizable === true;
15573 if (movable) {
15574 card.classList.add(MOVABLE_CLASS);
15575 }
15576 if (resizable) {
15577 card.classList.add(RESIZABLE_CLASS);
15578 }
15579 if (movable) {
15580 card.appendChild(buildChrome(def, handlers2.onRemove, handlers2.onRedock));
15581 } else {
15582 card.appendChild(buildCornerClose(def, handlers2.onRemove));
15583 }
15584 const body = document.createElement("div");
15585 body.className = "desktop-mode-widgets__card-body";
15586 card.appendChild(body);
15587 if (ctx.geometry) {
15588 applyGeometry(
15589 card,
15590 clampGeometryToParent(ctx.geometry, ctx.floatingParent)
15591 );
15592 card.classList.add(FLOATING_CLASS);
15593 } else if (resizable && typeof ctx.dockedHeight === "number") {
15594 card.style.height = `${clampDockedHeight(ctx.dockedHeight, def)}px`;
15595 }
15596 const isFloating = () => card.classList.contains(FLOATING_CLASS);
15597 const resizeCleanups = [];
15598 if (resizable) {
15599 for (const dir of allHandleDirs()) {
15600 const handle = document.createElement("div");
15601 handle.className = `desktop-mode-widgets__resize desktop-mode-widgets__resize--${dir}`;
15602 handle.setAttribute("aria-hidden", "true");
15603 handle.dataset.dir = dir;
15604 card.appendChild(handle);
15605 resizeCleanups.push(
15606 attachResize(card, handle, dir, def, ctx, handlers2, isFloating)
15607 );
15608 }
15609 }
15610 let dragCleanup = null;
15611 if (movable) {
15612 const chrome = card.querySelector(
15613 ".desktop-mode-widgets__chrome"
15614 );
15615 if (chrome) {
15616 dragCleanup = attachDrag(card, chrome, def, ctx, handlers2);
15617 }
15618 }
15619 return {
15620 card,
15621 body,
15622 dispose: () => {
15623 for (const fn of resizeCleanups) {
15624 try {
15625 fn();
15626 } catch {
15627 }
15628 }
15629 if (dragCleanup) {
15630 try {
15631 dragCleanup();
15632 } catch {
15633 }
15634 }
15635 card.remove();
15636 }
15637 };
15638 }
15639 function buildChrome(def, onRemove, onRedock) {
15640 const chrome = document.createElement("header");
15641 chrome.className = "desktop-mode-widgets__chrome";
15642 const grip = document.createElement("span");
15643 grip.className = "desktop-mode-widgets__grip";
15644 grip.setAttribute("aria-hidden", "true");
15645 chrome.appendChild(grip);
15646 const title = document.createElement("span");
15647 title.className = "desktop-mode-widgets__title";
15648 title.textContent = def.label;
15649 chrome.appendChild(title);
15650 chrome.appendChild(buildRedockButton(def, onRedock));
15651 const close = buildCloseButton(def, onRemove);
15652 chrome.appendChild(close);
15653 return chrome;
15654 }
15655 function buildRedockButton(def, onRedock) {
15656 const btn = document.createElement("button");
15657 btn.type = "button";
15658 btn.className = "desktop-mode-widgets__card-redock";
15659 btn.setAttribute(
15660 "aria-label",
15661 // translators: %s is the widget label (e.g., "Clock")
15662 sprintf(__("Dock %s back to widget column"), def.label)
15663 );
15664 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>';
15665 btn.addEventListener("click", (e) => {
15666 e.preventDefault();
15667 e.stopPropagation();
15668 onRedock();
15669 });
15670 btn.dataset.noDrag = "true";
15671 return btn;
15672 }
15673 function buildCornerClose(def, onRemove) {
15674 const close = buildCloseButton(def, onRemove);
15675 close.classList.add("desktop-mode-widgets__card-close--corner");
15676 return close;
15677 }
15678 function buildCloseButton(def, onRemove) {
15679 const close = document.createElement("button");
15680 close.type = "button";
15681 close.className = "desktop-mode-widgets__card-close";
15682 close.setAttribute("aria-label", sprintf(__("Remove %s"), def.label));
15683 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>';
15684 close.addEventListener("click", (e) => {
15685 e.preventDefault();
15686 e.stopPropagation();
15687 onRemove();
15688 });
15689 return close;
15690 }
15691 function attachDrag(card, chrome, def, ctx, handlers2) {
15692 let pointerId = null;
15693 let startX = 0;
15694 let startY = 0;
15695 let initialLeft = 0;
15696 let initialTop = 0;
15697 let committed = false;
15698 const onDown = (e) => {
15699 if (e.button !== 0) {
15700 return;
15701 }
15702 const target2 = e.target;
15703 if (target2 && target2.closest(DRAG_EXCLUDED_SELECTORS)) {
15704 return;
15705 }
15706 e.preventDefault();
15707 pointerId = e.pointerId;
15708 startX = e.clientX;
15709 startY = e.clientY;
15710 committed = false;
15711 initialLeft = parseFloat(card.style.left) || 0;
15712 initialTop = parseFloat(card.style.top) || 0;
15713 chrome.setPointerCapture(pointerId);
15714 };
15715 const commitDrag = () => {
15716 if (!card.classList.contains(FLOATING_CLASS)) {
15717 const parentRect = ctx.floatingParent.getBoundingClientRect();
15718 const rect = card.getBoundingClientRect();
15719 const initial = {
15720 x: rect.left - parentRect.left,
15721 y: rect.top - parentRect.top,
15722 width: rect.width || def.defaultWidth || DEFAULT_WIDTH$1,
15723 height: rect.height || def.defaultHeight || DEFAULT_HEIGHT$1
15724 };
15725 applyGeometry(card, initial);
15726 card.classList.add(FLOATING_CLASS);
15727 handlers2.onLiberate(initial);
15728 initialLeft = parseFloat(card.style.left) || 0;
15729 initialTop = parseFloat(card.style.top) || 0;
15730 }
15731 card.classList.add(DRAGGING_CLASS);
15732 };
15733 const onMove = (e) => {
15734 if (pointerId === null || e.pointerId !== pointerId) {
15735 return;
15736 }
15737 const dx = e.clientX - startX;
15738 const dy = e.clientY - startY;
15739 if (!committed) {
15740 if (dx * dx + dy * dy < DRAG_THRESHOLD_SQUARED) {
15741 return;
15742 }
15743 committed = true;
15744 commitDrag();
15745 }
15746 const clamped = clampToParent(
15747 initialLeft + dx,
15748 initialTop + dy,
15749 card.offsetWidth,
15750 card.offsetHeight,
15751 ctx.floatingParent
15752 );
15753 card.style.left = `${clamped.x}px`;
15754 card.style.top = `${clamped.y}px`;
15755 };
15756 const onUp = (e) => {
15757 if (pointerId === null || e.pointerId !== pointerId) {
15758 return;
15759 }
15760 try {
15761 chrome.releasePointerCapture(pointerId);
15762 } catch {
15763 }
15764 pointerId = null;
15765 if (!committed) {
15766 return;
15767 }
15768 committed = false;
15769 card.classList.remove(DRAGGING_CLASS);
15770 handlers2.onGeometryChanged(currentGeometry(card));
15771 };
15772 chrome.addEventListener("pointerdown", onDown);
15773 chrome.addEventListener("pointermove", onMove);
15774 chrome.addEventListener("pointerup", onUp);
15775 chrome.addEventListener("pointercancel", onUp);
15776 return () => {
15777 chrome.removeEventListener("pointerdown", onDown);
15778 chrome.removeEventListener("pointermove", onMove);
15779 chrome.removeEventListener("pointerup", onUp);
15780 chrome.removeEventListener("pointercancel", onUp);
15781 };
15782 }
15783 function attachResize(card, handle, dir, def, ctx, handlers2, isFloating) {
15784 let pointerId = null;
15785 let startX = 0;
15786 let startY = 0;
15787 let startLeft = 0;
15788 let startTop = 0;
15789 let startW = 0;
15790 let startH = 0;
15791 const onDown = (e) => {
15792 if (e.button !== 0) {
15793 return;
15794 }
15795 if (!isFloating() && !isHeightOnlyDir(dir)) {
15796 return;
15797 }
15798 e.preventDefault();
15799 e.stopPropagation();
15800 pointerId = e.pointerId;
15801 startX = e.clientX;
15802 startY = e.clientY;
15803 const rect = card.getBoundingClientRect();
15804 const parentRect = ctx.floatingParent.getBoundingClientRect();
15805 startLeft = rect.left - parentRect.left;
15806 startTop = rect.top - parentRect.top;
15807 startW = rect.width;
15808 startH = rect.height;
15809 handle.setPointerCapture(pointerId);
15810 card.classList.add(RESIZING_CLASS);
15811 };
15812 const onMove = (e) => {
15813 if (pointerId === null || e.pointerId !== pointerId) {
15814 return;
15815 }
15816 const dx = e.clientX - startX;
15817 const dy = e.clientY - startY;
15818 const next = computeResize(
15819 dir,
15820 dx,
15821 dy,
15822 startLeft,
15823 startTop,
15824 startW,
15825 startH,
15826 def,
15827 ctx.floatingParent,
15828 isFloating()
15829 );
15830 if (isFloating()) {
15831 card.style.left = `${next.x}px`;
15832 card.style.top = `${next.y}px`;
15833 card.style.width = `${next.width}px`;
15834 }
15835 card.style.height = `${next.height}px`;
15836 };
15837 const onUp = (e) => {
15838 if (pointerId === null || e.pointerId !== pointerId) {
15839 return;
15840 }
15841 try {
15842 handle.releasePointerCapture(pointerId);
15843 } catch {
15844 }
15845 pointerId = null;
15846 card.classList.remove(RESIZING_CLASS);
15847 if (isFloating()) {
15848 handlers2.onGeometryChanged(currentGeometry(card));
15849 } else {
15850 handlers2.onDockedHeightChanged(card.offsetHeight);
15851 }
15852 };
15853 handle.addEventListener("pointerdown", onDown);
15854 handle.addEventListener("pointermove", onMove);
15855 handle.addEventListener("pointerup", onUp);
15856 handle.addEventListener("pointercancel", onUp);
15857 return () => {
15858 handle.removeEventListener("pointerdown", onDown);
15859 handle.removeEventListener("pointermove", onMove);
15860 handle.removeEventListener("pointerup", onUp);
15861 handle.removeEventListener("pointercancel", onUp);
15862 };
15863 }
15864 function allHandleDirs() {
15865 return ["n", "e", "s", "w", "ne", "nw", "se", "sw"];
15866 }
15867 function isHeightOnlyDir(dir) {
15868 return dir === "s";
15869 }
15870 function applyGeometry(card, geometry) {
15871 card.style.left = `${geometry.x}px`;
15872 card.style.top = `${geometry.y}px`;
15873 card.style.width = `${geometry.width}px`;
15874 card.style.height = `${geometry.height}px`;
15875 }
15876 function currentGeometry(card) {
15877 return {
15878 x: parseFloat(card.style.left) || 0,
15879 y: parseFloat(card.style.top) || 0,
15880 width: card.offsetWidth,
15881 height: card.offsetHeight
15882 };
15883 }
15884 function clampDockedHeight(height, def) {
15885 return clamp$1(
15886 height,
15887 def.minHeight ?? DEFAULT_MIN_HEIGHT,
15888 def.maxHeight ?? Infinity
15889 );
15890 }
15891 function clampGeometryToParent(geometry, parent) {
15892 if (!parent.clientWidth || !parent.clientHeight) {
15893 return geometry;
15894 }
15895 const clamped = clampToParent(
15896 geometry.x,
15897 geometry.y,
15898 geometry.width,
15899 geometry.height,
15900 parent
15901 );
15902 return { ...geometry, x: clamped.x, y: clamped.y };
15903 }
15904 function clampToParent(x, y, width, height, parent) {
15905 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
15906 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
15907 const maxX = Math.max(0, parentWidth - width - VIEWPORT_MARGIN);
15908 const maxY = Math.max(0, parentHeight - height - VIEWPORT_MARGIN);
15909 return {
15910 x: Math.min(Math.max(VIEWPORT_MARGIN, x), maxX),
15911 y: Math.min(Math.max(VIEWPORT_MARGIN, y), maxY)
15912 };
15913 }
15914 function computeResize(dir, dx, dy, startLeft, startTop, startW, startH, def, parent, floating) {
15915 const minW = def.minWidth ?? DEFAULT_MIN_WIDTH;
15916 const minH = def.minHeight ?? DEFAULT_MIN_HEIGHT;
15917 const maxW = def.maxWidth ?? Infinity;
15918 const maxH = def.maxHeight ?? Infinity;
15919 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
15920 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
15921 let x = startLeft;
15922 let y = startTop;
15923 let width = startW;
15924 let height = startH;
15925 if (dir === "e" || dir === "ne" || dir === "se") {
15926 width = clamp$1(startW + dx, minW, Math.min(maxW, parentWidth - startLeft));
15927 }
15928 if (dir === "w" || dir === "nw" || dir === "sw") {
15929 const nextWidth = clamp$1(startW - dx, minW, Math.min(maxW, startLeft + startW));
15930 x = startLeft + (startW - nextWidth);
15931 width = nextWidth;
15932 }
15933 if (dir === "s" || dir === "se" || dir === "sw") {
15934 height = clamp$1(
15935 startH + dy,
15936 minH,
15937 Math.min(maxH, parentHeight - startTop)
15938 );
15939 }
15940 if (dir === "n" || dir === "ne" || dir === "nw") {
15941 const nextHeight = clamp$1(startH - dy, minH, Math.min(maxH, startTop + startH));
15942 y = startTop + (startH - nextHeight);
15943 height = nextHeight;
15944 }
15945 if (!floating) {
15946 width = startW;
15947 x = startLeft;
15948 }
15949 return { x, y, width, height };
15950 }
15951 function clamp$1(value, min, max) {
15952 if (max < min) {
15953 return min;
15954 }
15955 return Math.min(Math.max(value, min), max);
15956 }
15957 const IDS_KEY = "desktop-mode-widgets";
15958 const GEOMETRY_KEY$1 = "desktop-mode-widgets-geometry";
15959 const DOCKED_HEIGHTS_KEY = "desktop-mode-widgets-docked-heights";
15960 function readRawEnabled() {
15961 try {
15962 return window.localStorage.getItem(IDS_KEY);
15963 } catch {
15964 return null;
15965 }
15966 }
15967 function loadEnabledIds() {
15968 const raw = readRawEnabled();
15969 if (raw === null) {
15970 return [];
15971 }
15972 try {
15973 const parsed = JSON.parse(raw);
15974 if (!Array.isArray(parsed)) {
15975 return [];
15976 }
15977 return parsed.filter((x) => typeof x === "string");
15978 } catch {
15979 return [];
15980 }
15981 }
15982 function saveEnabledIds(ids) {
15983 try {
15984 window.localStorage.setItem(IDS_KEY, JSON.stringify(ids));
15985 } catch {
15986 }
15987 }
15988 function loadGeometry$1() {
15989 try {
15990 const raw = window.localStorage.getItem(GEOMETRY_KEY$1);
15991 if (!raw) {
15992 return {};
15993 }
15994 const parsed = JSON.parse(raw);
15995 if (!parsed || typeof parsed !== "object") {
15996 return {};
15997 }
15998 const out = {};
15999 for (const [id, rawEntry] of Object.entries(parsed)) {
16000 const entry = sanitizeGeometry(rawEntry);
16001 if (entry) {
16002 out[id] = entry;
16003 }
16004 }
16005 return out;
16006 } catch {
16007 return {};
16008 }
16009 }
16010 function saveGeometry$1(geometry) {
16011 try {
16012 window.localStorage.setItem(GEOMETRY_KEY$1, JSON.stringify(geometry));
16013 } catch {
16014 }
16015 }
16016 function loadDockedHeights() {
16017 try {
16018 const raw = window.localStorage.getItem(DOCKED_HEIGHTS_KEY);
16019 if (!raw) {
16020 return {};
16021 }
16022 const parsed = JSON.parse(raw);
16023 if (!parsed || typeof parsed !== "object") {
16024 return {};
16025 }
16026 const out = {};
16027 for (const [id, value] of Object.entries(parsed)) {
16028 if (typeof value === "number" && Number.isFinite(value) && value > 0) {
16029 out[id] = value;
16030 }
16031 }
16032 return out;
16033 } catch {
16034 return {};
16035 }
16036 }
16037 function saveDockedHeights(heights) {
16038 try {
16039 window.localStorage.setItem(
16040 DOCKED_HEIGHTS_KEY,
16041 JSON.stringify(heights)
16042 );
16043 } catch {
16044 }
16045 }
16046 function sanitizeGeometry(raw) {
16047 if (!raw || typeof raw !== "object") {
16048 return null;
16049 }
16050 const { x, y, width, height } = raw;
16051 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) {
16052 return null;
16053 }
16054 return { x, y, width, height };
16055 }
16056 function createWidgetStorage(widgetId) {
16057 const prefix = `desktop-mode.widget.${widgetId}.`;
16058 const safeGet = (key) => {
16059 try {
16060 return localStorage.getItem(prefix + key);
16061 } catch {
16062 return null;
16063 }
16064 };
16065 return {
16066 get(key) {
16067 const raw = safeGet(key);
16068 if (raw === null) {
16069 return null;
16070 }
16071 try {
16072 return JSON.parse(raw);
16073 } catch {
16074 return null;
16075 }
16076 },
16077 set(key, value) {
16078 try {
16079 localStorage.setItem(prefix + key, JSON.stringify(value));
16080 } catch {
16081 }
16082 },
16083 remove(key) {
16084 try {
16085 localStorage.removeItem(prefix + key);
16086 } catch {
16087 }
16088 },
16089 clear() {
16090 try {
16091 for (let i = localStorage.length - 1; i >= 0; i--) {
16092 const key = localStorage.key(i);
16093 if (key && key.startsWith(prefix)) {
16094 localStorage.removeItem(key);
16095 }
16096 }
16097 } catch {
16098 }
16099 }
16100 };
16101 }
16102 const DEFAULT_ENABLED_IDS = ["clock"];
16103 class WidgetLayer {
16104 /**
16105 * @param root The column element (`#desktop-mode-widgets`).
16106 * @param pluginUrl Absolute plugin URL — passed to widget ctx.
16107 * @param floatingHost Parent for liberated (floating) widgets.
16108 * Defaults to the column's parent (the desktop
16109 * area) so floats are bounded by the visible
16110 * desktop, not the 320 px-wide column.
16111 */
16112 constructor(root, pluginUrl, floatingHost) {
16113 this.mounted = /* @__PURE__ */ new Map();
16114 this.generation = 0;
16115 this.root = root;
16116 this.pluginUrl = pluginUrl;
16117 this.enabledIds = loadEnabledIds();
16118 this.geometry = loadGeometry$1();
16119 this.dockedHeights = loadDockedHeights();
16120 this.floatingHost = floatingHost ?? root.parentElement ?? root;
16121 this.listEl = document.createElement("div");
16122 this.listEl.className = "desktop-mode-widgets__list";
16123 this.root.appendChild(this.listEl);
16124 this.addTile = this.buildAddTile();
16125 this.root.appendChild(this.addTile);
16126 this.paintEmptyState();
16127 }
16128 /**
16129 * Mount every widget the user has enabled (per localStorage).
16130 * Called once during shell boot, AFTER the registry seed has run
16131 * so built-ins are available. Safe to call multiple times — the
16132 * `mounted` map dedupes.
16133 */
16134 hydrate() {
16135 if (readRawEnabled() === null) {
16136 this.enabledIds = DEFAULT_ENABLED_IDS.filter(
16137 (id) => !!get(id)
16138 );
16139 saveEnabledIds(this.enabledIds);
16140 }
16141 for (const id of this.enabledIds) {
16142 if (this.mounted.has(id)) {
16143 continue;
16144 }
16145 this.mountById(id);
16146 }
16147 this.paintEmptyState();
16148 }
16149 /**
16150 * Add a widget by id — called by the picker after the user
16151 * selects an available entry. Idempotent.
16152 */
16153 add(id) {
16154 if (this.enabledIds.includes(id)) {
16155 return;
16156 }
16157 if (!get(id)) {
16158 return;
16159 }
16160 this.enabledIds.push(id);
16161 saveEnabledIds(this.enabledIds);
16162 this.mountById(id);
16163 this.paintEmptyState();
16164 doAction(HOOKS.WIDGET_ADDED, { id });
16165 refreshWidgetPicker();
16166 }
16167 /**
16168 * Remove a widget by id — called from the card's × button and
16169 * from the picker. Idempotent.
16170 */
16171 remove(id) {
16172 const before = this.enabledIds.length;
16173 this.enabledIds = this.enabledIds.filter((e) => e !== id);
16174 if (this.enabledIds.length === before) {
16175 return;
16176 }
16177 saveEnabledIds(this.enabledIds);
16178 if (this.geometry[id]) {
16179 delete this.geometry[id];
16180 saveGeometry$1(this.geometry);
16181 }
16182 if (this.dockedHeights[id] !== void 0) {
16183 delete this.dockedHeights[id];
16184 saveDockedHeights(this.dockedHeights);
16185 }
16186 this.unmountById(id);
16187 this.paintEmptyState();
16188 doAction(HOOKS.WIDGET_REMOVED, { id });
16189 refreshWidgetPicker();
16190 }
16191 /** Public read for the picker / external callers. */
16192 getEnabledIds() {
16193 return [...this.enabledIds];
16194 }
16195 /**
16196 * Mount a widget ONLY if it's already in the user's enabled
16197 * list AND not currently mounted. No-op when the widget isn't
16198 * enabled (user never opted in) and no-op when it's already on
16199 * screen. Used by the server-driven sync: when a plugin
16200 * activates mid-session, its widget def registers via the
16201 * sync's path; if the user had previously enabled that widget
16202 * (in a prior session or before the plugin was deactivated),
16203 * we want to bring it back on screen without toggling the
16204 * "enabled" state or firing a `WIDGET_ADDED` action.
16205 *
16206 * The net behaviour is "rehydrate this one widget now that
16207 * its def is finally registered," which is subtly different
16208 * from `ensureMounted` (which OPT-INs the user into enabling
16209 * the widget for the first time).
16210 */
16211 mountIfEnabled(id) {
16212 if (!get(id)) {
16213 return;
16214 }
16215 if (!this.enabledIds.includes(id)) {
16216 return;
16217 }
16218 if (this.mounted.has(id)) {
16219 return;
16220 }
16221 this.mountById(id);
16222 this.paintEmptyState();
16223 }
16224 /**
16225 * Unmount a widget without touching the persisted enablement.
16226 * Used by the server-driven widget-registry sync: when a plugin
16227 * deactivates mid-session, its widget defs disappear from the
16228 * registry and we need to pull any mounted instance off the
16229 * screen — but we deliberately KEEP the id in the user's
16230 * enabled list so re-activating the plugin re-mounts it
16231 * automatically through `hydrate()`.
16232 *
16233 * Idempotent; a no-op when the widget isn't currently mounted.
16234 */
16235 unmount(id) {
16236 if (!this.mounted.has(id)) {
16237 return;
16238 }
16239 this.unmountById(id);
16240 this.paintEmptyState();
16241 }
16242 /**
16243 * Guarantee the widget identified by `id` is currently mounted,
16244 * adding it to the enabled list if it isn't. No-op when the
16245 * widget is already on screen. Intended for companion plugins
16246 * that want to pin their widget programmatically — a monitor
16247 * plugin that auto-pins itself on the first error burst, a
16248 * first-run onboarding flow that ensures the quick-start widget
16249 * is present, etc.
16250 *
16251 * Returns `true` when the widget is mounted (either newly added
16252 * or already present), `false` when the id isn't registered —
16253 * callers can branch on the failure without having to maintain
16254 * their own registry snapshot.
16255 */
16256 ensureMounted(id) {
16257 if (!get(id)) {
16258 return false;
16259 }
16260 if (this.enabledIds.includes(id)) {
16261 return true;
16262 }
16263 this.add(id);
16264 return true;
16265 }
16266 /**
16267 * Tear down every widget. Called on shell unload via `pagehide`
16268 * so intervals / RAF loops stop before the beacon flush.
16269 */
16270 disposeAll() {
16271 for (const id of Array.from(this.mounted.keys())) {
16272 this.unmountById(id);
16273 }
16274 }
16275 // --- Internal ---------------------------------------------------
16276 mountById(id) {
16277 const def = get(id);
16278 if (!def) {
16279 return;
16280 }
16281 const gen = ++this.generation;
16282 const initialGeometry = def.movable === true ? this.geometry[id] : void 0;
16283 const frame = buildFrame(
16284 def,
16285 {
16286 floatingParent: this.floatingHost,
16287 geometry: initialGeometry,
16288 dockedHeight: this.dockedHeights[id]
16289 },
16290 {
16291 onRemove: () => this.remove(id),
16292 onGeometryChanged: (geom) => this.persistGeometry(id, geom),
16293 onDockedHeightChanged: (height) => this.persistDockedHeight(id, height),
16294 onLiberate: (geom) => this.liberate(id, geom),
16295 onRedock: () => this.redock(id)
16296 }
16297 );
16298 const floating = !!initialGeometry;
16299 const record = {
16300 id,
16301 frame,
16302 generation: gen,
16303 teardown: null,
16304 floating
16305 };
16306 this.mounted.set(id, record);
16307 this.placeCard(frame.card, floating);
16308 const ctx = {
16309 id,
16310 pluginUrl: this.pluginUrl,
16311 storage: createWidgetStorage(id)
16312 };
16313 doAction(HOOKS.WIDGET_MOUNTING, { id, container: frame.body, ctx });
16314 const onResolve = (teardown) => {
16315 const current = this.mounted.get(id);
16316 if (!current || current.generation !== gen) {
16317 try {
16318 teardown();
16319 } catch {
16320 }
16321 return;
16322 }
16323 current.teardown = teardown;
16324 doAction(HOOKS.WIDGET_MOUNTED, { id, container: frame.body, ctx });
16325 };
16326 let result;
16327 try {
16328 result = def.mount(frame.body, ctx);
16329 } catch (err) {
16330 this.handleMountFailure(id, err);
16331 return;
16332 }
16333 if (isThenable(result)) {
16334 result.then(onResolve, (err) => {
16335 if (this.mounted.get(id)?.generation === gen) {
16336 this.handleMountFailure(id, err);
16337 }
16338 });
16339 return;
16340 }
16341 onResolve(result);
16342 }
16343 unmountById(id) {
16344 const record = this.mounted.get(id);
16345 if (!record) {
16346 return;
16347 }
16348 doAction(HOOKS.WIDGET_UNMOUNTING, { id });
16349 try {
16350 record.teardown?.();
16351 } catch (err) {
16352 doAction(HOOKS.SHELL_ERROR, { scope: "widget-teardown", id, error: err });
16353 if (typeof console !== "undefined") {
16354 console.error(
16355 `[desktop-mode] Widget "${id}" teardown threw:`,
16356 err
16357 );
16358 }
16359 }
16360 this.generation++;
16361 record.frame.dispose();
16362 this.mounted.delete(id);
16363 }
16364 handleMountFailure(id, err) {
16365 const record = this.mounted.get(id);
16366 if (record) {
16367 record.frame.dispose();
16368 this.mounted.delete(id);
16369 }
16370 doAction(HOOKS.WIDGET_MOUNT_FAILED, { id, error: err });
16371 doAction(HOOKS.SHELL_ERROR, { scope: "widget-mount", id, error: err });
16372 if (typeof console !== "undefined") {
16373 console.error(
16374 `[desktop-mode] Widget "${id}" failed to mount:`,
16375 err
16376 );
16377 }
16378 }
16379 buildAddTile() {
16380 const tile2 = document.createElement("button");
16381 tile2.type = "button";
16382 tile2.className = "desktop-mode-widgets__add";
16383 tile2.setAttribute("aria-label", __("Add widget"));
16384 const plus = document.createElement("span");
16385 plus.className = "desktop-mode-widgets__add-plus";
16386 plus.setAttribute("aria-hidden", "true");
16387 plus.textContent = "+";
16388 const label = document.createElement("span");
16389 label.className = "desktop-mode-widgets__add-label";
16390 label.textContent = __("Add widget");
16391 tile2.appendChild(plus);
16392 tile2.appendChild(label);
16393 tile2.addEventListener("click", (e) => {
16394 e.preventDefault();
16395 e.stopPropagation();
16396 openWidgetPicker({
16397 anchor: tile2,
16398 registry: () => all(),
16399 enabledIds: () => [...this.enabledIds],
16400 onAdd: (id) => this.add(id)
16401 });
16402 });
16403 return tile2;
16404 }
16405 /**
16406 * Drop a card into the right parent based on its floating state.
16407 * Docked cards append to the column list above the `+` tile;
16408 * floating cards append to the desktop-area-level host so they
16409 * sit above the wallpaper and can range across the viewport.
16410 */
16411 placeCard(card, floating) {
16412 if (floating) {
16413 this.floatingHost.appendChild(card);
16414 } else {
16415 this.listEl.appendChild(card);
16416 }
16417 }
16418 /**
16419 * Move a widget from the column into the floating host. Called by
16420 * the frame on the user's first drag of a movable widget.
16421 */
16422 liberate(id, geometry) {
16423 const record = this.mounted.get(id);
16424 if (!record || record.floating) {
16425 return;
16426 }
16427 record.floating = true;
16428 this.floatingHost.appendChild(record.frame.card);
16429 applyGeometry(record.frame.card, geometry);
16430 this.persistGeometry(id, geometry);
16431 this.paintEmptyState();
16432 }
16433 /**
16434 * Inverse of {@link liberate}: move a floating card back into
16435 * the column and drop its persisted geometry so a subsequent
16436 * shell boot brings it up docked. Called when the user clicks
16437 * the re-dock button in the card's chrome header, or
16438 * programmatically by companion plugins via
16439 * `wp.desktop.widgets.redock( id )` /
16440 * `wp.desktop.widgetLayer.redock( id )`.
16441 *
16442 * Idempotent — a docked widget silently no-ops, an unknown id
16443 * silently no-ops. The `--floating` class on the card is
16444 * removed as part of the same write so CSS rules that depend
16445 * on it (re-dock button visibility, absolute positioning) flip
16446 * back in one paint.
16447 *
16448 * @since 0.7.0 (private)
16449 * @since 0.8.6 (public)
16450 */
16451 redock(id) {
16452 const record = this.mounted.get(id);
16453 if (!record || !record.floating) {
16454 return;
16455 }
16456 record.floating = false;
16457 if (this.geometry[id]) {
16458 delete this.geometry[id];
16459 saveGeometry$1(this.geometry);
16460 }
16461 const card = record.frame.card;
16462 card.classList.remove("desktop-mode-widgets__card--floating");
16463 card.style.left = "";
16464 card.style.top = "";
16465 card.style.width = "";
16466 const dockedHeight = this.dockedHeights[id];
16467 card.style.height = dockedHeight !== void 0 ? `${dockedHeight}px` : "";
16468 this.listEl.appendChild(card);
16469 this.paintEmptyState();
16470 }
16471 persistGeometry(id, geometry) {
16472 this.geometry[id] = geometry;
16473 saveGeometry$1(this.geometry);
16474 }
16475 persistDockedHeight(id, height) {
16476 if (!Number.isFinite(height) || height <= 0) {
16477 return;
16478 }
16479 this.dockedHeights[id] = height;
16480 saveDockedHeights(this.dockedHeights);
16481 }
16482 /**
16483 * Toggle a `--has-widgets` modifier so CSS can hide the column's
16484 * decorative backdrop when nothing's mounted (keeps the empty
16485 * state clean — just the `+` tile floating in the corner).
16486 *
16487 * Floating widgets don't count toward "has widgets" in the column
16488 * sense — if every enabled widget is floating, the column itself
16489 * shows only the empty state + add tile.
16490 */
16491 paintEmptyState() {
16492 let docked = 0;
16493 for (const record of this.mounted.values()) {
16494 if (!record.floating) {
16495 docked++;
16496 }
16497 }
16498 this.root.classList.toggle(
16499 "desktop-mode-widgets--has-widgets",
16500 docked > 0
16501 );
16502 }
16503 }
16504 function isThenable(x) {
16505 return !!x && (typeof x === "object" || typeof x === "function") && typeof x.then === "function";
16506 }
16507 const DEFAULT_NATIVE_MIN_WIDTH = 280;
16508 const DEFAULT_NATIVE_MIN_HEIGHT = 220;
16509 const DEFAULT_NATIVE_WIDTH = 520;
16510 const DEFAULT_NATIVE_HEIGHT = 400;
16511 function buildIframeContentRender(cfg, cleanups, windowId) {
16512 return (body) => {
16513 const iframe = document.createElement("iframe");
16514 iframe.style.width = "100%";
16515 iframe.style.height = "100%";
16516 iframe.style.border = "0";
16517 iframe.setAttribute("src", cfg.url);
16518 if (typeof cfg.sandbox === "string" && cfg.sandbox !== "") {
16519 iframe.setAttribute("sandbox", cfg.sandbox);
16520 }
16521 body.style.padding = "0";
16522 body.appendChild(iframe);
16523 const unregisterSynth = registerSyntheticIframe(windowId, iframe);
16524 cleanups.push(unregisterSynth);
16525 let targetOrigin;
16526 try {
16527 targetOrigin = new URL(cfg.url, window.location.origin).origin;
16528 } catch {
16529 targetOrigin = window.location.origin;
16530 }
16531 let resolveReady = null;
16532 const readyPromise = new Promise((resolve2) => {
16533 resolveReady = resolve2;
16534 });
16535 const onLoad = () => {
16536 if (cfg.bridge) {
16537 try {
16538 const doc = iframe.contentDocument;
16539 if (doc && !doc.querySelector("script[data-desktop-mode-iframe-bridge]")) {
16540 const bridgeUrl = window.desktopModeConfig?.iframeBridgeUrl;
16541 if (bridgeUrl) {
16542 const s = doc.createElement("script");
16543 s.src = bridgeUrl;
16544 s.setAttribute("data-desktop-mode-iframe-bridge", "1");
16545 doc.head?.appendChild(s);
16546 }
16547 }
16548 } catch {
16549 }
16550 }
16551 markWindowContentReady(windowId);
16552 resolveReady?.();
16553 };
16554 iframe.addEventListener("load", onLoad);
16555 const onMessage = (e) => {
16556 if (!iframe.contentWindow || e.source !== iframe.contentWindow) {
16557 return;
16558 }
16559 if (e.origin !== targetOrigin && e.origin !== window.location.origin) {
16560 return;
16561 }
16562 const data = e.data;
16563 if (data && typeof data === "object" && typeof data.type === "string" && data.type.startsWith("desktop-mode-bridge-")) {
16564 const bridgeRouter = window.__desktopModeConnectionBridge;
16565 bridgeRouter?.routeIncomingFromIframe(data, windowId);
16566 }
16567 if (data && typeof data === "object" && data.type === "desktop-mode-window-publish" && typeof data.channel === "string" && data.channel !== "") {
16568 dispatchFromWindow(
16569 windowId,
16570 data.channel,
16571 data.payload
16572 );
16573 }
16574 try {
16575 cfg.onMessage?.(e.data);
16576 } catch (err) {
16577 if (typeof console !== "undefined") {
16578 console.error(
16579 "[desktop-mode] iframeContent.onMessage threw:",
16580 err
16581 );
16582 }
16583 }
16584 };
16585 window.addEventListener("message", onMessage);
16586 cleanups.push(() => {
16587 window.removeEventListener("message", onMessage);
16588 iframe.removeEventListener("load", onLoad);
16589 });
16590 return readyPromise;
16591 };
16592 }
16593 function createRegisterWindow(manager2) {
16594 return async (def) => {
16595 const userRender = def.render;
16596 let render2 = userRender;
16597 const cleanups = [];
16598 if (def.iframeContent) {
16599 if (userRender && typeof console !== "undefined") {
16600 console.warn(
16601 "[desktop-mode] registerWindow: both `render` and `iframeContent` provided — ignoring `render` and using the iframe shorthand. Drop one."
16602 );
16603 }
16604 render2 = buildIframeContentRender(
16605 def.iframeContent,
16606 cleanups,
16607 def.id
16608 );
16609 }
16610 const userOnClose = def.onClose;
16611 const onClose = cleanups.length ? () => {
16612 for (const fn of cleanups) {
16613 try {
16614 fn();
16615 } catch {
16616 }
16617 }
16618 userOnClose?.();
16619 } : userOnClose;
16620 const win = await manager2.open({
16621 id: def.id,
16622 baseId: def.baseId || def.id,
16623 native: true,
16624 url: def.url || `#${def.id}`,
16625 title: def.title,
16626 icon: def.icon,
16627 x: def.x ?? 0,
16628 y: def.y ?? 0,
16629 width: def.width ?? DEFAULT_NATIVE_WIDTH,
16630 height: def.height ?? DEFAULT_NATIVE_HEIGHT,
16631 minWidth: def.minWidth ?? DEFAULT_NATIVE_MIN_WIDTH,
16632 minHeight: def.minHeight ?? DEFAULT_NATIVE_MIN_HEIGHT,
16633 render: render2,
16634 onClose,
16635 onResize: def.onResize,
16636 autofocus: def.autofocus,
16637 initialState: def.initialState,
16638 ownerHandle: def.ownerHandle,
16639 multi: def.multi,
16640 desktopId: def.desktopId
16641 });
16642 return win;
16643 };
16644 }
16645 let onWindowInstanceCounter = 0;
16646 function onWindow(id, handlers2, options = {}) {
16647 const namespace = `desktop-mode/on-window/${id}/${++onWindowInstanceCounter}`;
16648 const persistent = options.persistent === true;
16649 const bindings = [
16650 ["opened", HOOKS.WINDOW_OPENED],
16651 ["reopened", HOOKS.WINDOW_REOPENED],
16652 ["focused", HOOKS.WINDOW_FOCUSED],
16653 ["blurred", HOOKS.WINDOW_BLURRED],
16654 ["closing", HOOKS.WINDOW_CLOSING],
16655 ["closed", HOOKS.WINDOW_CLOSED],
16656 ["minimized", HOOKS.WINDOW_MINIMIZED],
16657 ["restored", HOOKS.WINDOW_RESTORED],
16658 ["maximized", HOOKS.WINDOW_MAXIMIZED],
16659 ["unmaximized", HOOKS.WINDOW_UNMAXIMIZED],
16660 ["fullscreenEntered", HOOKS.WINDOW_FULLSCREEN_ENTERED],
16661 ["fullscreenExited", HOOKS.WINDOW_FULLSCREEN_EXITED],
16662 ["resized", HOOKS.WINDOW_RESIZED],
16663 ["bodyResized", HOOKS.WINDOW_BODY_RESIZED],
16664 ["boundsChanged", HOOKS.WINDOW_BOUNDS_CHANGED]
16665 ];
16666 const registered = [];
16667 let disposed = false;
16668 const unsubscribe = () => {
16669 if (disposed) {
16670 return;
16671 }
16672 disposed = true;
16673 for (const hookName2 of registered) {
16674 removeAction(hookName2, namespace);
16675 }
16676 };
16677 for (const [key, hookName2] of bindings) {
16678 const handler = handlers2[key];
16679 if (!handler) {
16680 continue;
16681 }
16682 registered.push(hookName2);
16683 addAction(hookName2, namespace, (payload) => {
16684 const p = payload;
16685 if (p.windowId !== id) {
16686 return;
16687 }
16688 const { windowId: _w, ...rest } = p;
16689 handler(rest);
16690 if (key === "closed" && !persistent) {
16691 unsubscribe();
16692 }
16693 });
16694 }
16695 return unsubscribe;
16696 }
16697 function readGlobalRegistry() {
16698 const g = window;
16699 return {
16700 ...g.wpDesktopNativeWindows || {},
16701 ...g.desktopModeNativeWindows || {}
16702 };
16703 }
16704 function createNativeWindowSync(deps2) {
16705 const { manager: manager2, appendSystemTile, removeSystemTile } = deps2;
16706 const registered = /* @__PURE__ */ new Set();
16707 const injectedTemplates = /* @__PURE__ */ new Set();
16708 const loadedScripts = /* @__PURE__ */ new Set();
16709 const loadedStyles = /* @__PURE__ */ new Set();
16710 const entriesById = /* @__PURE__ */ new Map();
16711 const resolveSizeForEntry = (entry) => {
16712 const saved = loadNativeWindowGeometry(entry.id);
16713 if (!saved) {
16714 return { width: entry.width, height: entry.height };
16715 }
16716 return {
16717 width: Math.max(saved.width, entry.minWidth),
16718 height: Math.max(saved.height, entry.minHeight)
16719 };
16720 };
16721 const ensureTemplate = (entry) => {
16722 if (injectedTemplates.has(entry.templateId)) {
16723 return;
16724 }
16725 if (document.getElementById(entry.templateId)) {
16726 injectedTemplates.add(entry.templateId);
16727 return;
16728 }
16729 if (!entry.templateHtml) {
16730 return;
16731 }
16732 const tpl = document.createElement("template");
16733 tpl.id = entry.templateId;
16734 tpl.innerHTML = entry.templateHtml;
16735 document.body.appendChild(tpl);
16736 injectedTemplates.add(entry.templateId);
16737 };
16738 const ensureStyle = (entry) => {
16739 const url = entry.styleUrl;
16740 if (!url || loadedStyles.has(url)) {
16741 return;
16742 }
16743 const safeUrl = url.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
16744 const existing = document.head.querySelector(
16745 `link[rel="stylesheet"][href="${safeUrl}"]`
16746 );
16747 if (!existing) {
16748 const link = document.createElement("link");
16749 link.rel = "stylesheet";
16750 link.href = url;
16751 if (entry.styleHandle) {
16752 link.dataset.desktopModeStyleHandle = entry.styleHandle;
16753 }
16754 document.head.appendChild(link);
16755 }
16756 if (Array.isArray(entry.styleInline)) {
16757 for (const css2 of entry.styleInline) {
16758 if (typeof css2 !== "string" || css2 === "") {
16759 continue;
16760 }
16761 const style = document.createElement("style");
16762 if (entry.styleHandle) {
16763 style.dataset.desktopModeStyleHandle = entry.styleHandle;
16764 }
16765 style.textContent = css2;
16766 document.head.appendChild(style);
16767 }
16768 }
16769 loadedStyles.add(url);
16770 };
16771 const ensureScript = async (entry) => {
16772 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
16773 return;
16774 }
16775 try {
16776 await loadVendorScript(entry.scriptUrl, {
16777 translations: entry.scriptTranslations,
16778 l10n: entry.scriptL10n,
16779 before: entry.scriptBefore,
16780 after: entry.scriptAfter
16781 });
16782 } catch (err) {
16783 doAction(HOOKS.SHELL_ERROR, {
16784 scope: "native-window-script-load",
16785 id: entry.id,
16786 error: err
16787 });
16788 }
16789 loadedScripts.add(entry.scriptUrl);
16790 };
16791 const openFromEntry = (entry) => {
16792 const render2 = readGlobalRegistry()[entry.id];
16793 const finalRender = (body, ctx) => {
16794 body.appendChild(cloneTemplate(entry.templateId));
16795 return render2?.(body, ctx);
16796 };
16797 const size = resolveSizeForEntry(entry);
16798 void manager2.open({
16799 id: entry.id,
16800 baseId: entry.id,
16801 native: true,
16802 url: `#${entry.id}`,
16803 title: entry.title,
16804 icon: entry.icon,
16805 width: size.width,
16806 height: size.height,
16807 minWidth: entry.minWidth,
16808 minHeight: entry.minHeight,
16809 render: finalRender,
16810 autofocus: entry.autofocus,
16811 ownerHandle: entry.ownerHandle || entry.scriptHandle
16812 });
16813 };
16814 const openNewFromEntry = (entry) => {
16815 const render2 = readGlobalRegistry()[entry.id];
16816 const finalRender = (body, ctx) => {
16817 body.appendChild(cloneTemplate(entry.templateId));
16818 return render2?.(body, ctx);
16819 };
16820 const size = resolveSizeForEntry(entry);
16821 void manager2.openNew({
16822 id: entry.id,
16823 baseId: entry.id,
16824 native: true,
16825 url: `#${entry.id}`,
16826 title: entry.title,
16827 icon: entry.icon,
16828 width: size.width,
16829 height: size.height,
16830 minWidth: entry.minWidth,
16831 minHeight: entry.minHeight,
16832 initialState: "normal",
16833 render: finalRender,
16834 autofocus: entry.autofocus,
16835 ownerHandle: entry.ownerHandle || entry.scriptHandle
16836 });
16837 };
16838 const registerTile = async (entry) => {
16839 if (registered.has(entry.id)) {
16840 return;
16841 }
16842 if ("none" === entry.placement) {
16843 ensureTemplate(entry);
16844 ensureStyle(entry);
16845 await ensureScript(entry);
16846 registered.add(entry.id);
16847 return;
16848 }
16849 ensureTemplate(entry);
16850 ensureStyle(entry);
16851 await ensureScript(entry);
16852 appendSystemTile({
16853 id: entry.id,
16854 title: entry.title,
16855 icon: entry.icon,
16856 isOpen: () => !!manager2.getById(entry.id),
16857 onOpen: () => openFromEntry(entry)
16858 });
16859 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: entry.id });
16860 registered.add(entry.id);
16861 };
16862 const unregisterTile = (id) => {
16863 if (!registered.has(id)) {
16864 return;
16865 }
16866 removeSystemTile(id);
16867 registered.delete(id);
16868 entriesById.delete(id);
16869 };
16870 const sync = async (list2) => {
16871 const incoming = /* @__PURE__ */ new Set();
16872 for (const entry of list2) {
16873 incoming.add(entry.id);
16874 entriesById.set(entry.id, entry);
16875 }
16876 for (const id of Array.from(registered)) {
16877 if (!incoming.has(id)) {
16878 unregisterTile(id);
16879 }
16880 }
16881 for (const entry of list2) {
16882 if (!registered.has(entry.id)) {
16883 await registerTile(entry);
16884 }
16885 }
16886 };
16887 const openById = (id, opts = {}) => {
16888 const entry = entriesById.get(id);
16889 if (!entry) {
16890 return false;
16891 }
16892 activity.publish("desktop-mode/open-requested", {
16893 windowId: id,
16894 source: opts.source ?? "api"
16895 });
16896 openFromEntry(entry);
16897 return true;
16898 };
16899 const openNewById = (id, opts = {}) => {
16900 const entry = entriesById.get(id);
16901 if (!entry) {
16902 return false;
16903 }
16904 activity.publish("desktop-mode/open-requested", {
16905 windowId: id,
16906 source: opts.source ?? "api"
16907 });
16908 openNewFromEntry(entry);
16909 return true;
16910 };
16911 addAction(
16912 HOOKS.WINDOW_RESIZE_END,
16913 "desktop-mode-native-window-geometry",
16914 (payload) => {
16915 const p = payload;
16916 const windowId = p?.windowId;
16917 const width = p?.width;
16918 const height = p?.height;
16919 if (!windowId || typeof width !== "number" || typeof height !== "number") {
16920 return;
16921 }
16922 const win = manager2.getById(windowId);
16923 if (!win) {
16924 return;
16925 }
16926 if (win.state !== "normal") {
16927 return;
16928 }
16929 const baseId = win.config.baseId || win.id;
16930 saveNativeWindowGeometry(baseId, { width, height });
16931 if (win.element) {
16932 saveNativeWindowPosition(baseId, {
16933 x: win.element.offsetLeft,
16934 y: win.element.offsetTop
16935 });
16936 }
16937 }
16938 );
16939 addAction(
16940 HOOKS.WINDOW_DRAG_END,
16941 "desktop-mode-native-window-geometry",
16942 (payload) => {
16943 const windowId = payload?.windowId;
16944 if (!windowId) {
16945 return;
16946 }
16947 const win = manager2.getById(windowId);
16948 if (!win) {
16949 return;
16950 }
16951 if (win.state !== "normal") {
16952 return;
16953 }
16954 if (!win.element) {
16955 return;
16956 }
16957 const baseId = win.config.baseId || win.id;
16958 saveNativeWindowGeometry(baseId, {
16959 width: win.element.offsetWidth,
16960 height: win.element.offsetHeight
16961 });
16962 saveNativeWindowPosition(baseId, {
16963 x: win.element.offsetLeft,
16964 y: win.element.offsetTop
16965 });
16966 }
16967 );
16968 addAction(
16969 HOOKS.WINDOW_MAXIMIZED,
16970 "desktop-mode-native-window-geometry",
16971 (payload) => {
16972 const windowId = payload?.windowId;
16973 if (!windowId) {
16974 return;
16975 }
16976 const win = manager2.getById(windowId);
16977 if (!win) {
16978 return;
16979 }
16980 const baseId = win.config.baseId || win.id;
16981 const entry = entriesById.get(baseId);
16982 const defaults = entry ? { width: entry.width, height: entry.height } : { width: win.config.width, height: win.config.height };
16983 setNativeWindowSavedState(baseId, "maximized", defaults);
16984 }
16985 );
16986 addAction(
16987 HOOKS.WINDOW_UNMAXIMIZED,
16988 "desktop-mode-native-window-geometry",
16989 (payload) => {
16990 const windowId = payload?.windowId;
16991 if (!windowId) {
16992 return;
16993 }
16994 const win = manager2.getById(windowId);
16995 if (!win) {
16996 return;
16997 }
16998 const baseId = win.config.baseId || win.id;
16999 setNativeWindowSavedState(baseId, null);
17000 }
17001 );
17002 return { sync, openById, openNewById };
17003 }
17004 function cloneTemplate(template) {
17005 let tpl = null;
17006 if (typeof template === "string") {
17007 const found = document.getElementById(template);
17008 if (found instanceof HTMLTemplateElement) {
17009 tpl = found;
17010 }
17011 } else {
17012 tpl = template;
17013 }
17014 if (!tpl) {
17015 throw new Error(
17016 `[desktop-mode] cloneTemplate: no <template> found for ${typeof template === "string" ? `#${template}` : "<reference>"}`
17017 );
17018 }
17019 return tpl.content.cloneNode(true);
17020 }
17021 function findMenuEntryForUrl(url) {
17022 const wp = window.wp?.desktop;
17023 const bootConfig = window.desktopModeConfig;
17024 const adminUrl = wp?.config?.adminUrl ?? bootConfig?.adminUrl;
17025 if (!adminUrl) {
17026 return null;
17027 }
17028 const items = wp?.getMenuItems?.() ?? bootConfig?.dockItems ?? [];
17029 const targetId = deriveWindowId(url, adminUrl);
17030 return items.find(
17031 (item) => deriveWindowId(item.url, adminUrl) === targetId || (item.submenu ?? []).some(
17032 (sub) => deriveWindowId(sub.url, adminUrl) === targetId
17033 )
17034 ) ?? null;
17035 }
17036 const BADGE_CLASS = "desktop-mode-icon__badge";
17037 const _badges = /* @__PURE__ */ new Map();
17038 function _safeBadge(count) {
17039 return Math.max(0, Math.floor(Number(count) || 0));
17040 }
17041 function setIconBadge(iconId, count) {
17042 if (!iconId) {
17043 return;
17044 }
17045 const tile2 = _findIconTile(iconId);
17046 if (!tile2) {
17047 return;
17048 }
17049 const safe = _safeBadge(count);
17050 const previous = _badges.get(iconId) ?? 0;
17051 if (safe === previous) {
17052 return;
17053 }
17054 if (safe === 0) {
17055 _badges.delete(iconId);
17056 } else {
17057 _badges.set(iconId, safe);
17058 }
17059 _paintBadgeNode(tile2, safe);
17060 activity.publish("desktop-mode/badge-changed", {
17061 itemId: iconId,
17062 count: safe,
17063 rail: "icon"
17064 });
17065 doAction(HOOKS.ICON_BADGE_CHANGED, {
17066 iconId,
17067 count: safe,
17068 previousCount: previous
17069 });
17070 }
17071 function clearIconBadge(iconId) {
17072 setIconBadge(iconId, 0);
17073 }
17074 function getIconBadge(iconId) {
17075 return _badges.get(iconId) ?? 0;
17076 }
17077 const iconsApi = {
17078 setBadge: setIconBadge,
17079 clearBadge: clearIconBadge,
17080 getBadge: getIconBadge
17081 };
17082 function fingerprintIcons(icons) {
17083 if (!icons || icons.length === 0) {
17084 return "";
17085 }
17086 return icons.map(
17087 (i) => `${i.id}|${i.title}|${i.icon}|${i.window ?? ""}|${i.url ?? ""}|${i.position ?? 0}|${i.pinned ? 1 : 0}`
17088 ).join(";");
17089 }
17090 let _lastFingerprint = "";
17091 function renderDesktopIcons(host, icons, deps2) {
17092 const fp = fingerprintIcons(icons);
17093 if (fp === _lastFingerprint && host.querySelector(":scope > .desktop-mode-icons")) {
17094 return;
17095 }
17096 _lastFingerprint = fp;
17097 const existing = host.querySelector(":scope > .desktop-mode-icons");
17098 if (existing) {
17099 existing.remove();
17100 }
17101 if (!icons || icons.length === 0) {
17102 return;
17103 }
17104 const container = document.createElement("div");
17105 container.className = "desktop-mode-icons";
17106 container.setAttribute("role", "list");
17107 container.setAttribute("aria-label", __("Desktop icons"));
17108 const ordered = [...icons].sort((a, b) => {
17109 const ap = a.pinned ? 0 : 1;
17110 const bp = b.pinned ? 0 : 1;
17111 return ap - bp;
17112 });
17113 const tiles = /* @__PURE__ */ new Map();
17114 for (const entry of ordered) {
17115 const tile2 = buildIcon(entry, deps2);
17116 const stored = _badges.get(entry.id) ?? 0;
17117 if (stored > 0) {
17118 _paintBadgeNode(tile2, stored);
17119 }
17120 container.appendChild(tile2);
17121 tiles.set(entry.id, tile2);
17122 }
17123 host.appendChild(container);
17124 doAction(HOOKS.DESKTOP_ICONS_RENDERED, {
17125 ids: (icons ?? []).map((i) => i.id),
17126 container,
17127 tiles
17128 });
17129 }
17130 function _findIconTile(iconId) {
17131 if (!iconId) {
17132 return null;
17133 }
17134 const container = document.querySelector(
17135 ".desktop-mode-icons"
17136 );
17137 if (!container) {
17138 return null;
17139 }
17140 return container.querySelector(
17141 `[data-icon-id="${_cssEscape(iconId)}"]`
17142 );
17143 }
17144 function _paintBadgeNode(host, count) {
17145 const existing = host.querySelector(
17146 `:scope > .${BADGE_CLASS}`
17147 );
17148 if (count <= 0) {
17149 existing?.remove();
17150 return;
17151 }
17152 const display = count > 99 ? "99+" : String(count);
17153 const ariaLabel = sprintf(
17154 // translators: %d is the number of pending items in a desktop-icon badge.
17155 _n("%d notification", "%d notifications", count),
17156 count
17157 );
17158 if (existing) {
17159 if (existing.textContent !== display) {
17160 existing.textContent = display;
17161 }
17162 existing.setAttribute("aria-label", ariaLabel);
17163 return;
17164 }
17165 const badge = document.createElement("span");
17166 badge.className = BADGE_CLASS;
17167 badge.textContent = display;
17168 badge.setAttribute("aria-label", ariaLabel);
17169 host.appendChild(badge);
17170 }
17171 function _cssEscape(value) {
17172 const c = window.CSS;
17173 return c?.escape ? c.escape(value) : value;
17174 }
17175 function buildIcon(entry, deps2) {
17176 const tile2 = document.createElement("button");
17177 tile2.type = "button";
17178 tile2.className = entry.pinned ? "desktop-mode-icon desktop-mode-icon--pinned" : "desktop-mode-icon";
17179 tile2.dataset.iconId = entry.id;
17180 if (entry.pinned) {
17181 tile2.dataset.pinned = "1";
17182 }
17183 tile2.setAttribute("role", "listitem");
17184 tile2.setAttribute("aria-label", entry.title);
17185 const icon = renderIcon(entry.icon, {
17186 title: entry.title,
17187 className: "desktop-mode-icon__image"
17188 });
17189 tile2.appendChild(icon);
17190 const label = document.createElement("span");
17191 label.className = "desktop-mode-icon__label";
17192 label.textContent = entry.title;
17193 tile2.appendChild(label);
17194 tile2.addEventListener("click", (e) => {
17195 e.stopPropagation();
17196 doAction(HOOKS.DESKTOP_ICON_CLICKED, {
17197 id: entry.id,
17198 target: entry.window ? "window" : "url"
17199 });
17200 openTarget(entry, deps2);
17201 });
17202 tile2.addEventListener("contextmenu", (e) => {
17203 if (entry.pinned) {
17204 return;
17205 }
17206 e.preventDefault();
17207 e.stopPropagation();
17208 openItemVisibilityMenu({
17209 x: e.clientX,
17210 y: e.clientY,
17211 id: entry.id,
17212 title: entry.title,
17213 surface: "desktop"
17214 });
17215 });
17216 return tile2;
17217 }
17218 function openTarget(entry, deps2) {
17219 if (entry.window) {
17220 const opened = deps2.openWindow(entry.window);
17221 if (!opened) {
17222 return;
17223 }
17224 return;
17225 }
17226 if (entry.url) {
17227 if (tryOpenExternalUrl(entry.url)) {
17228 return;
17229 }
17230 try {
17231 const parsed = new URL(entry.url, window.location.origin);
17232 const windowId = deps2.deriveWindowId(parsed.toString());
17233 const menuEntry = findMenuEntryForUrl(parsed.toString());
17234 void deps2.manager.open({
17235 id: windowId,
17236 baseId: windowId,
17237 url: parsed.toString(),
17238 parentUrl: menuEntry?.url ?? parsed.toString(),
17239 title: entry.title,
17240 icon: entry.icon,
17241 submenu: menuEntry?.submenu,
17242 multi: !!menuEntry?.multi
17243 });
17244 } catch {
17245 }
17246 }
17247 }
17248 const SIDE_DOCK_ID = "desktop-mode-side-dock";
17249 function coreItemToIconEntry(item, index2) {
17250 return {
17251 id: `dock-core:${item.id}`,
17252 title: item.title,
17253 icon: item.icon,
17254 window: "",
17255 url: item.url,
17256 // Synthesized icons render after server-registered ones; the
17257 // large offset leaves headroom for plugin authors who set
17258 // explicit `position` values.
17259 position: 1e3 + index2
17260 };
17261 }
17262 function createLayoutDispatcher(deps2, initialLayout, initialDockItems, initialServerIcons) {
17263 let layout = initialLayout;
17264 let items = initialDockItems;
17265 let serverIcons = initialServerIcons ?? [];
17266 let primary = null;
17267 let side = null;
17268 let primaryDock = null;
17269 let sideDock = null;
17270 let sideDockEl = null;
17271 const systemTiles = /* @__PURE__ */ new Map();
17272 const railFor = (affinity) => {
17273 if (affinity === "core" && side) {
17274 return side;
17275 }
17276 return primary;
17277 };
17278 const ensureSideDockEl = () => {
17279 const existing = document.getElementById(
17280 SIDE_DOCK_ID
17281 );
17282 if (existing) {
17283 return existing;
17284 }
17285 const el = document.createElement("nav");
17286 el.id = SIDE_DOCK_ID;
17287 el.className = "desktop-mode-dock";
17288 el.setAttribute("role", "toolbar");
17289 el.setAttribute("aria-label", "Core admin navigation");
17290 deps2.shellBody.insertBefore(el, deps2.shellBody.firstChild);
17291 return el;
17292 };
17293 const removeSideDockEl = () => {
17294 if (sideDockEl && sideDockEl.parentNode) {
17295 sideDockEl.parentNode.removeChild(sideDockEl);
17296 }
17297 sideDockEl = null;
17298 };
17299 const readSettings = () => deps2.getSettings?.() ?? { itemVisibility: {}, dockOrder: [] };
17300 const effectiveDockItems = () => {
17301 const dockedNativeWindows = /* @__PURE__ */ new Set();
17302 for (const entry of systemTiles.values()) {
17303 dockedNativeWindows.add(entry.item.id);
17304 }
17305 return applyDockPlacement(
17306 items,
17307 serverIcons,
17308 readSettings(),
17309 dockedNativeWindows
17310 );
17311 };
17312 const partition = () => {
17313 const effective = effectiveDockItems();
17314 const core = [];
17315 const plugin = [];
17316 for (const item of effective) {
17317 if (item.isCore) {
17318 core.push(item);
17319 } else {
17320 plugin.push(item);
17321 }
17322 }
17323 return { core, plugin };
17324 };
17325 const repaintIcons = () => {
17326 const settings = readSettings();
17327 if (layout !== "spatial") {
17328 deps2.renderIcons(
17329 applyDesktopPlacement(serverIcons, items, settings.itemVisibility)
17330 );
17331 return;
17332 }
17333 const { core } = partition();
17334 const synthesized = core.map(coreItemToIconEntry);
17335 const keptServerIcons = serverIcons.filter((icon) => {
17336 const override = settings.itemVisibility[icon.id];
17337 if (override) {
17338 return override === "desktop" || override === "both";
17339 }
17340 return Boolean(icon.pinned);
17341 });
17342 const explicitlyPromoted = [];
17343 let synthIndex = 0;
17344 for (const item of items) {
17345 const placement = settings.itemVisibility[item.id];
17346 if (placement === "desktop" || placement === "both") {
17347 explicitlyPromoted.push({
17348 id: `dock:${item.id}`,
17349 title: item.title,
17350 icon: item.icon,
17351 window: "",
17352 url: item.url || "",
17353 position: 2e3 + synthIndex++
17354 });
17355 }
17356 }
17357 deps2.renderIcons([
17358 ...synthesized,
17359 ...keptServerIcons,
17360 ...explicitlyPromoted
17361 ]);
17362 };
17363 const tearDownDocks = () => {
17364 if (primary) {
17365 try {
17366 primary.destroy();
17367 } catch (err) {
17368 doAction(HOOKS.SHELL_ERROR, {
17369 scope: "dock-rail-renderer/destroy",
17370 error: err
17371 });
17372 }
17373 primary = null;
17374 primaryDock = null;
17375 }
17376 if (side) {
17377 try {
17378 side.destroy();
17379 } catch (err) {
17380 doAction(HOOKS.SHELL_ERROR, {
17381 scope: "dock-rail-renderer/destroy",
17382 error: err
17383 });
17384 }
17385 side = null;
17386 sideDock = null;
17387 }
17388 };
17389 const mountRail = (mountDeps) => {
17390 const renderer = resolveActive();
17391 if (!renderer) {
17392 doAction(HOOKS.SHELL_ERROR, {
17393 scope: "dock-rail-renderer",
17394 message: "No dock rail renderer is registered."
17395 });
17396 return null;
17397 }
17398 try {
17399 return renderer.mount(mountDeps);
17400 } catch (err) {
17401 doAction(HOOKS.SHELL_ERROR, {
17402 scope: "dock-rail-renderer/mount",
17403 rendererId: renderer.id,
17404 error: err
17405 });
17406 if (renderer === defaultDockRailRenderer) {
17407 return null;
17408 }
17409 try {
17410 return defaultDockRailRenderer.mount(mountDeps);
17411 } catch {
17412 return null;
17413 }
17414 }
17415 };
17416 const buildMountDeps = (container, railItems, orientation) => ({
17417 container,
17418 items: railItems,
17419 // `fullMenu` is the complete admin-menu list. Renderers that
17420 // want to ignore the layout's partitioning (e.g., paint
17421 // every menu item in one ring regardless of `isCore`) read
17422 // this instead of `items`. Snapshot per-mount so a renderer
17423 // holding the array sees a stable list; live updates flow
17424 // through `replaceItems`.
17425 fullMenu: items.slice(),
17426 // Same idea for system tiles — OS Settings, plugin-owned
17427 // native-window launchers, etc. Lets a renderer apply
17428 // uniform treatment across menu + system cohorts in one
17429 // pass. Live updates flow through `appendSystemItem` /
17430 // `removeSystemItem`.
17431 fullSystemTiles: Array.from(systemTiles.values()).map(
17432 (entry) => entry.item
17433 ),
17434 orientation,
17435 windowManager: deps2.windowManager,
17436 adminUrl: deps2.adminUrl,
17437 // `openItem` / `openSubmenuPick` / `openSystemItem` are
17438 // routing callbacks for custom renderers. They mirror
17439 // exactly what the default renderer (`Dock.openPage` /
17440 // `Dock.openSubmenuPick`) does internally — same
17441 // `deriveWindowId(url, adminUrl)` call, same window-
17442 // config shape — so a custom renderer addresses the same
17443 // window with the same id at runtime. Switching renderer
17444 // mid-session doesn't lose the user's open windows.
17445 openItem: (item) => {
17446 const baseId = deriveWindowId(item.url, deps2.adminUrl);
17447 deps2.windowManager.open({
17448 id: baseId,
17449 baseId,
17450 url: item.url,
17451 parentUrl: item.url,
17452 title: item.title,
17453 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
17454 submenu: item.submenu,
17455 multi: !!item.multi
17456 });
17457 },
17458 openSubmenuPick: (item, sub) => {
17459 deps2.windowManager.open({
17460 id: deriveWindowId(sub.url, deps2.adminUrl),
17461 baseId: deriveWindowId(item.url, deps2.adminUrl),
17462 url: sub.url,
17463 // Pin the synthetic parent tab to the dock landing
17464 // page, not to the sub-page the user picked. Without
17465 // this, a submenu-pick (e.g. clicking "Editor" inside
17466 // Appearance's submenu popover) would open at
17467 // site-editor.php with no way back to themes.php.
17468 parentUrl: item.url,
17469 title: item.title,
17470 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
17471 submenu: item.submenu,
17472 multi: !!item.multi
17473 });
17474 },
17475 openSystemItem: (item) => item.onOpen()
17476 });
17477 const buildDocksForCurrentLayout = () => {
17478 tearDownDocks();
17479 const { core, plugin } = partition();
17480 if (layout === "classic") {
17481 sideDockEl = ensureSideDockEl();
17482 side = mountRail(
17483 buildMountDeps(sideDockEl, core, "left")
17484 );
17485 sideDock = unwrapDefaultDock(side);
17486 primary = mountRail(
17487 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
17488 );
17489 primaryDock = unwrapDefaultDock(primary);
17490 } else if (layout === "unified") {
17491 removeSideDockEl();
17492 primary = mountRail(
17493 buildMountDeps(deps2.bottomDockEl, effectiveDockItems(), "bottom")
17494 );
17495 primaryDock = unwrapDefaultDock(primary);
17496 } else {
17497 removeSideDockEl();
17498 primary = mountRail(
17499 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
17500 );
17501 primaryDock = unwrapDefaultDock(primary);
17502 }
17503 for (const entry of systemTiles.values()) {
17504 railFor(entry.affinity)?.appendSystemItem(entry.item);
17505 }
17506 };
17507 const dispatcher = {
17508 getLayout: () => layout,
17509 getPrimary: () => primaryDock,
17510 getSide: () => sideDock,
17511 setLayout: (next) => {
17512 if (next === layout) {
17513 return;
17514 }
17515 layout = next;
17516 deps2.shellRoot.setAttribute("data-desktop-mode-layout", next);
17517 buildDocksForCurrentLayout();
17518 repaintIcons();
17519 document.dispatchEvent(
17520 new CustomEvent("desktop-mode-layout-changed", {
17521 detail: {
17522 layout: next,
17523 primary: primaryDock,
17524 side: sideDock
17525 }
17526 })
17527 );
17528 },
17529 applyDockItems: (nextItems) => {
17530 items = nextItems;
17531 const { core, plugin } = partition();
17532 if (layout === "classic") {
17533 side?.replaceItems(core);
17534 primary?.replaceItems(plugin);
17535 } else if (layout === "unified") {
17536 primary?.replaceItems(effectiveDockItems());
17537 } else {
17538 primary?.replaceItems(plugin);
17539 }
17540 repaintIcons();
17541 },
17542 applyDesktopIcons: (next) => {
17543 serverIcons = next ?? [];
17544 repaintIcons();
17545 },
17546 appendSystemTile: (item, affinity = "plugin") => {
17547 systemTiles.set(item.id, { item, affinity });
17548 railFor(affinity)?.appendSystemItem(item);
17549 },
17550 removeSystemTile: (id) => {
17551 const entry = systemTiles.get(id);
17552 if (!entry) {
17553 return;
17554 }
17555 systemTiles.delete(id);
17556 railFor(entry.affinity)?.removeSystemItem(id);
17557 },
17558 listSystemTiles: () => Array.from(systemTiles.values()).map((entry) => ({
17559 id: entry.item.id,
17560 title: entry.item.title,
17561 icon: entry.item.icon,
17562 affinity: entry.affinity
17563 })),
17564 getSystemTile: (id) => systemTiles.get(id)?.item ?? null,
17565 getMenuItems: () => items.slice(),
17566 refresh: () => {
17567 const { core, plugin } = partition();
17568 if (layout === "classic") {
17569 side?.replaceItems(core);
17570 primary?.replaceItems(plugin);
17571 } else if (layout === "unified") {
17572 primary?.replaceItems(effectiveDockItems());
17573 } else {
17574 primary?.replaceItems(plugin);
17575 }
17576 repaintIcons();
17577 },
17578 destroy: () => {
17579 tearDownDocks();
17580 removeSideDockEl();
17581 }
17582 };
17583 deps2.shellRoot.setAttribute("data-desktop-mode-layout", layout);
17584 buildDocksForCurrentLayout();
17585 repaintIcons();
17586 let lastResolvedId = resolveActive()?.id ?? null;
17587 subscribe$4(() => {
17588 const nextId2 = resolveActive()?.id ?? null;
17589 if (nextId2 === lastResolvedId) {
17590 return;
17591 }
17592 lastResolvedId = nextId2;
17593 buildDocksForCurrentLayout();
17594 repaintIcons();
17595 document.dispatchEvent(
17596 new CustomEvent("desktop-mode-layout-changed", {
17597 detail: {
17598 layout,
17599 primary: primaryDock,
17600 side: sideDock
17601 }
17602 })
17603 );
17604 });
17605 return dispatcher;
17606 }
17607 function loadImpl(scriptUrl) {
17608 if (window.desktopModeCreateAiAssistant) {
17609 return Promise.resolve(window.desktopModeCreateAiAssistant);
17610 }
17611 return new Promise((resolve2, reject) => {
17612 const existing = document.querySelector(
17613 `script[data-desktop-mode-ai="1"]`
17614 );
17615 const finish = () => {
17616 const factory = window.desktopModeCreateAiAssistant;
17617 if (!factory) {
17618 reject(
17619 new Error(
17620 "[desktop-mode] ai-assistant bundle loaded but did not register desktopModeCreateAiAssistant"
17621 )
17622 );
17623 return;
17624 }
17625 resolve2(factory);
17626 };
17627 if (existing) {
17628 if (window.desktopModeCreateAiAssistant) {
17629 finish();
17630 } else {
17631 existing.addEventListener("load", finish);
17632 existing.addEventListener(
17633 "error",
17634 () => reject(new Error("failed to load ai-assistant bundle"))
17635 );
17636 }
17637 return;
17638 }
17639 const s = document.createElement("script");
17640 s.src = scriptUrl;
17641 s.async = true;
17642 s.dataset.desktopModeAi = "1";
17643 s.addEventListener("load", finish);
17644 s.addEventListener(
17645 "error",
17646 () => reject(new Error("failed to load ai-assistant bundle"))
17647 );
17648 document.head.appendChild(s);
17649 });
17650 }
17651 class AiAssistantStub {
17652 constructor(config, scriptUrl) {
17653 this._real = null;
17654 this._loadPromise = null;
17655 this._pendingAsk = null;
17656 this._intendOpen = false;
17657 this.ask = (...args) => {
17658 return this._ensure().then((r) => r.ask(...args));
17659 };
17660 this._config = config;
17661 this._scriptUrl = scriptUrl;
17662 }
17663 _ensure() {
17664 if (this._loadPromise) {
17665 return this._loadPromise;
17666 }
17667 this._loadPromise = loadImpl(this._scriptUrl).then((factory) => {
17668 const real = factory(this._config);
17669 if (this._pendingAsk) {
17670 real.attachAsk(this._pendingAsk);
17671 }
17672 this._real = real;
17673 return real;
17674 });
17675 return this._loadPromise;
17676 }
17677 open() {
17678 this._intendOpen = true;
17679 void this._ensure().then((r) => r.open());
17680 }
17681 close() {
17682 this._intendOpen = false;
17683 if (this._real) {
17684 this._real.close();
17685 }
17686 }
17687 toggle() {
17688 if (this.isOpen) {
17689 this.close();
17690 } else {
17691 this.open();
17692 }
17693 }
17694 get isOpen() {
17695 return this._real ? this._real.isOpen : this._intendOpen;
17696 }
17697 /**
17698 * Late-bind the programmatic `ask` callback. Mirrors the real
17699 * class's `attachAsk` signature so `desktop.ts`'s call site is
17700 * identical whether it's wiring the stub or the impl.
17701 */
17702 attachAsk(fn) {
17703 this._pendingAsk = fn;
17704 if (this._real) {
17705 this._real.attachAsk(fn);
17706 }
17707 }
17708 }
17709 const isAbortError = (err) => {
17710 if (!err || typeof err !== "object") {
17711 return false;
17712 }
17713 return err.name === "AbortError";
17714 };
17715 const normaliseToolsOpt = (tools) => {
17716 if (!tools) {
17717 return [];
17718 }
17719 const all2 = listAiCallableCommands();
17720 if (tools === true || tools === "aiCallable") {
17721 return all2;
17722 }
17723 if (Array.isArray(tools)) {
17724 const allowed = new Set(tools.map((s) => s.toLowerCase()));
17725 return all2.filter((c) => allowed.has(c.slug));
17726 }
17727 if (typeof tools === "function") {
17728 return all2.filter((c) => {
17729 try {
17730 return tools(c.slug) === true;
17731 } catch {
17732 return false;
17733 }
17734 });
17735 }
17736 return [];
17737 };
17738 const normaliseSystemPrompt = (sp) => {
17739 if (!sp) {
17740 return null;
17741 }
17742 if (typeof sp === "string") {
17743 return { text: sp, mode: "append" };
17744 }
17745 if (typeof sp === "object" && typeof sp.text === "string" && sp.text !== "") {
17746 return {
17747 text: sp.text,
17748 mode: sp.mode === "replace" ? "replace" : "append"
17749 };
17750 }
17751 return null;
17752 };
17753 function liftMessage(payloadMessage, result) {
17754 const seed2 = payloadMessage ?? "";
17755 if (seed2 !== "") {
17756 return seed2;
17757 }
17758 if (typeof result === "string" && result !== "") {
17759 return result;
17760 }
17761 if (result && typeof result === "object" && "message" in result && typeof result.message === "string") {
17762 return result.message;
17763 }
17764 return "";
17765 }
17766 function serialiseOutcome(result) {
17767 if (result === void 0) {
17768 return { value: null };
17769 }
17770 if (typeof result === "object" && result !== null) {
17771 return result;
17772 }
17773 return { value: result };
17774 }
17775 function createAsk(deps2) {
17776 const postToSearch = async (body, signal) => {
17777 const config = deps2.config();
17778 const url = config.aiSearchUrl ?? "";
17779 const nonce = config.restNonce ?? "";
17780 if (!url || !nonce) {
17781 throw new Error(
17782 "[desktop-mode] wp.desktop.ai.ask: aiSearchUrl / restNonce missing from config. AI Copilot may not be enabled."
17783 );
17784 }
17785 try {
17786 return await trackedFetch$1(
17787 url,
17788 {
17789 method: "POST",
17790 credentials: "same-origin",
17791 headers: {
17792 "Content-Type": "application/json",
17793 "X-WP-Nonce": nonce
17794 },
17795 body: JSON.stringify(body),
17796 signal
17797 },
17798 { source: "desktop-mode/ai-ask" }
17799 );
17800 } catch (err) {
17801 if (isAbortError(err)) {
17802 throw err;
17803 }
17804 throw new Error(
17805 `[desktop-mode] wp.desktop.ai.ask: network error — ${String(
17806 err?.message ?? err
17807 )}`
17808 );
17809 }
17810 };
17811 const dispatchToolCall = async (payload, opts) => {
17812 const slug = payload.tool?.slug ?? "";
17813 const args = payload.tool?.args ?? "";
17814 const cmd = findCommand(slug);
17815 if (!cmd) {
17816 return {
17817 ok: false,
17818 response: {
17819 answer_type: "tool_call",
17820 message: `Command /${slug} was not registered on this page.`,
17821 entity: null,
17822 admin_links: null,
17823 toolCall: {
17824 slug,
17825 args,
17826 result: { error: "command_not_found" }
17827 },
17828 request_id: payload.request_id
17829 }
17830 };
17831 }
17832 const ctx = opts.commandContext ?? deps2.fallbackContext();
17833 let result;
17834 try {
17835 result = await Promise.resolve(cmd.run(args, ctx));
17836 } catch (err) {
17837 result = { error: String(err?.message ?? err) };
17838 }
17839 return { ok: true, slug, args, result };
17840 };
17841 const composeFollowUp = async (text, slug, args, result, sp, signal) => {
17842 const body = {
17843 query: text,
17844 follow_up: {
17845 tool: { slug, args },
17846 result: serialiseOutcome(result)
17847 }
17848 };
17849 if (sp) {
17850 body.system_prompt_text = sp.text;
17851 body.system_prompt_mode = sp.mode;
17852 }
17853 let res;
17854 try {
17855 res = await postToSearch(body, signal);
17856 } catch (err) {
17857 if (isAbortError(err)) {
17858 throw err;
17859 }
17860 return null;
17861 }
17862 if (!res.ok) {
17863 return null;
17864 }
17865 const payload = await res.json().catch(() => ({}));
17866 const message = typeof payload.message === "string" ? payload.message.trim() : "";
17867 return message !== "" ? payload.message ?? null : null;
17868 };
17869 return async function ask(query, opts = {}) {
17870 const text = (query ?? "").trim();
17871 if (text === "") {
17872 const hasMeaningfulOpts = opts.tools !== void 0 || opts.systemPrompt !== void 0 || opts.followUp === true || opts.resumeTool !== void 0 || opts.commandContext !== void 0;
17873 if (hasMeaningfulOpts) {
17874 throw new Error(
17875 "[desktop-mode] wp.desktop.ai.ask: empty query passed with non-default options — likely a caller bug. Provide a query or call without options."
17876 );
17877 }
17878 return {
17879 answer_type: "chat",
17880 message: "",
17881 entity: null,
17882 admin_links: null
17883 };
17884 }
17885 const commandTools = normaliseToolsOpt(opts.tools);
17886 const sp = normaliseSystemPrompt(opts.systemPrompt);
17887 const body = { query: text };
17888 if (opts.resumeTool) {
17889 body.resume_tool = opts.resumeTool;
17890 }
17891 if (typeof opts.startOffset === "number") {
17892 body.start_offset = opts.startOffset;
17893 }
17894 if (commandTools.length > 0) {
17895 body.command_tools = commandTools;
17896 }
17897 if (sp) {
17898 body.system_prompt_text = sp.text;
17899 body.system_prompt_mode = sp.mode;
17900 }
17901 const res = await postToSearch(body, opts.signal);
17902 if (!res.ok) {
17903 const detail = await res.json().catch(() => ({ message: res.statusText }));
17904 throw new Error(
17905 `[desktop-mode] wp.desktop.ai.ask: HTTP ${res.status} — ${detail.message ?? res.statusText}`
17906 );
17907 }
17908 const payload = await res.json();
17909 if (payload.answer_type !== "tool_call" || !payload.tool) {
17910 return {
17911 answer_type: payload.answer_type,
17912 message: payload.message ?? "",
17913 entity: payload.entity ?? null,
17914 admin_links: payload.admin_links ?? null,
17915 request_id: payload.request_id,
17916 continue: payload.continue ?? null
17917 };
17918 }
17919 const dispatch2 = await dispatchToolCall(payload, opts);
17920 if (!dispatch2.ok) {
17921 return dispatch2.response;
17922 }
17923 const { slug, args, result } = dispatch2;
17924 let message = liftMessage(payload.message, result);
17925 if (opts.followUp === true) {
17926 const composed = await composeFollowUp(
17927 text,
17928 slug,
17929 args,
17930 result,
17931 sp,
17932 opts.signal
17933 );
17934 if (composed !== null) {
17935 message = composed;
17936 }
17937 }
17938 return {
17939 answer_type: "tool_call",
17940 message,
17941 entity: null,
17942 admin_links: null,
17943 toolCall: { slug, args, result },
17944 request_id: payload.request_id
17945 };
17946 };
17947 }
17948 const EVENT_NAME = "desktop-mode-broadcast";
17949 const POSTMESSAGE_TYPE = "desktop-mode-broadcast";
17950 const ORIGIN = window.location.origin;
17951 let _manager = null;
17952 function attachBroadcastBus(manager2) {
17953 _manager = manager2;
17954 }
17955 function broadcast(topic, payload) {
17956 const filteredTopic = String(
17957 applyFilters("desktop-mode.broadcast.topic", topic, { payload }) ?? topic
17958 );
17959 const filteredPayload = applyFilters(
17960 "desktop-mode.broadcast.payload",
17961 payload,
17962 { topic: filteredTopic }
17963 );
17964 const detail = {
17965 topic: filteredTopic,
17966 payload: filteredPayload
17967 };
17968 document.dispatchEvent(new CustomEvent(EVENT_NAME, { detail }));
17969 doAction(HOOKS.BROADCAST, detail);
17970 activity.publish(
17971 filteredTopic,
17972 filteredPayload
17973 );
17974 if (!_manager) {
17975 return;
17976 }
17977 const message = {
17978 type: POSTMESSAGE_TYPE,
17979 topic: filteredTopic,
17980 payload: filteredPayload
17981 };
17982 for (const win of _manager._stack) {
17983 const target2 = win.iframe?.contentWindow;
17984 if (!target2) {
17985 continue;
17986 }
17987 try {
17988 target2.postMessage(message, ORIGIN);
17989 } catch (err) {
17990 }
17991 }
17992 }
17993 function subscribe$2(topic, cb) {
17994 const handler = (e) => {
17995 const detail = e.detail;
17996 if (!detail) {
17997 return;
17998 }
17999 if (topic !== "*" && detail.topic !== topic) {
18000 return;
18001 }
18002 try {
18003 cb(detail.payload, { topic: detail.topic });
18004 } catch (err) {
18005 doAction(HOOKS.SHELL_ERROR, {
18006 scope: "broadcast-subscriber",
18007 topic: detail.topic,
18008 error: err
18009 });
18010 }
18011 };
18012 document.addEventListener(EVENT_NAME, handler);
18013 return () => document.removeEventListener(EVENT_NAME, handler);
18014 }
18015 function installBroadcastReceiver() {
18016 window.addEventListener("message", (e) => {
18017 if (e.origin !== ORIGIN) {
18018 return;
18019 }
18020 const data = e.data;
18021 if (!data || data.type !== POSTMESSAGE_TYPE) {
18022 return;
18023 }
18024 if (data._fromParent) {
18025 return;
18026 }
18027 if (typeof data.topic !== "string") {
18028 return;
18029 }
18030 broadcast(data.topic, data.payload);
18031 });
18032 }
18033 const LOG_PREFIX = "[desktop-mode-bin badge]";
18034 function log(...args) {
18035 try {
18036 if (window.localStorage?.getItem("desktopModeBinDebug")) {
18037 console.info(LOG_PREFIX, ...args);
18038 }
18039 } catch {
18040 }
18041 }
18042 function warn(...args) {
18043 console.warn(LOG_PREFIX, ...args);
18044 }
18045 const TARGET_ID = "desktop-mode-recycle-bin";
18046 const HEARTBEAT_FIELD$1 = "desktop_mode_recycle_bin_seen_ts";
18047 function getDesktopApi$1() {
18048 return window.wp?.desktop;
18049 }
18050 const store$3 = createSharedStore(
18051 "desktop-mode/recycle-bin/badge",
18052 () => ({
18053 current: 0,
18054 seenTs: 0,
18055 started: false,
18056 countUrl: ""
18057 })
18058 );
18059 function setRecycleBinBadge(next) {
18060 const safe = Math.max(0, Math.floor(next));
18061 const prev = store$3.state.current;
18062 store$3.state.current = safe;
18063 log("setRecycleBinBadge", { prev, next: safe });
18064 paintBadge(safe);
18065 }
18066 function adjustRecycleBinBadge(delta) {
18067 setRecycleBinBadge(store$3.state.current + delta);
18068 }
18069 function _currentRecycleBinBadge() {
18070 return store$3.state.current;
18071 }
18072 function paintBadge(count) {
18073 const desktop = getDesktopApi$1();
18074 const active2 = isBinWindowActive();
18075 const visible = active2 ? 0 : count;
18076 log("paintBadge", { count, visible, active: active2 });
18077 desktop?.dock?.setBadge?.(TARGET_ID, visible);
18078 desktop?.taskbar?.setBadge?.(TARGET_ID, visible);
18079 desktop?.icons?.setBadge?.(TARGET_ID, visible);
18080 }
18081 function isBinWindowActive() {
18082 const mgr = getDesktopApi$1()?.windowManager;
18083 if (mgr?.isActiveByBaseId) {
18084 return mgr.isActiveByBaseId(TARGET_ID);
18085 }
18086 return !!mgr?.isActive?.(TARGET_ID);
18087 }
18088 function startRecycleBinBadge(initialRaw, countUrl = "") {
18089 const initial = Number(initialRaw) || 0;
18090 const cfg = window.desktopModeConfig;
18091 const cfgCount = cfg?.recycleBinCount;
18092 const cfgUrl = cfg?.recycleBinCountUrl;
18093 const cfgDebug = cfg?.desktopModeBinDebug;
18094 log("startRecycleBinBadge entry", {
18095 initial,
18096 countUrl,
18097 alreadyStarted: store$3.state.started,
18098 cfgCount,
18099 cfgUrl,
18100 cfgDebug,
18101 readyState: document.readyState
18102 });
18103 const cfgCountNum = Number(cfgCount);
18104 const cfgCountIsHealthy = (typeof cfgCount === "number" || typeof cfgCount === "string") && Number.isFinite(cfgCountNum);
18105 if (!cfgCountIsHealthy) {
18106 warn(
18107 "desktopModeConfig.recycleBinCount is missing — PHP filter `desktop_mode_shell_config` did not deliver. Check your PHP error log for `[desktop-mode-bin debug]` lines.",
18108 { cfg }
18109 );
18110 }
18111 if (store$3.state.started) {
18112 setRecycleBinBadge(initial);
18113 return;
18114 }
18115 store$3.state.started = true;
18116 store$3.state.countUrl = countUrl;
18117 store$3.state.seenTs = Date.now();
18118 setRecycleBinBadge(initial);
18119 wireDockTileSignal();
18120 wireDesktopIconsSignal();
18121 wireBroadcastDeltas();
18122 wirePostMessageFastPath();
18123 wireHeartbeatProbe();
18124 wireWindowLifecycleSignals();
18125 }
18126 function wireWindowLifecycleSignals() {
18127 const ns = "desktop-mode/recycle-bin/badge-lifecycle";
18128 const repaint = (payload) => {
18129 const detail = payload;
18130 const windowId = detail?.windowId;
18131 if (!windowId) {
18132 return;
18133 }
18134 const isBin = windowId === TARGET_ID || windowId.startsWith(TARGET_ID + "-");
18135 if (!isBin) {
18136 return;
18137 }
18138 paintBadge(store$3.state.current);
18139 };
18140 addAction(HOOKS.WINDOW_OPENED, ns, repaint);
18141 addAction(HOOKS.WINDOW_FOCUSED, ns, repaint);
18142 addAction(HOOKS.WINDOW_BLURRED, ns, repaint);
18143 addAction(HOOKS.WINDOW_MINIMIZED, ns, repaint);
18144 addAction(HOOKS.WINDOW_RESTORED, ns, repaint);
18145 addAction(HOOKS.WINDOW_CLOSED, ns, repaint);
18146 addAction(HOOKS.WINDOW_REOPENED, ns, repaint);
18147 }
18148 function wireDockTileSignal() {
18149 addAction(
18150 HOOKS.DOCK_ITEM_APPENDED,
18151 "desktop-mode/recycle-bin/badge",
18152 (payload) => {
18153 if (payload?.id === TARGET_ID) {
18154 paintBadge(store$3.state.current);
18155 }
18156 }
18157 );
18158 }
18159 function wireDesktopIconsSignal() {
18160 addAction(
18161 HOOKS.DESKTOP_ICONS_RENDERED,
18162 "desktop-mode/recycle-bin/badge",
18163 (payload) => {
18164 if (payload?.ids?.includes(TARGET_ID)) {
18165 paintBadge(store$3.state.current);
18166 }
18167 }
18168 );
18169 }
18170 function wireBroadcastDeltas() {
18171 const onDomain = (payload) => {
18172 const detail = payload;
18173 if (!detail) {
18174 return;
18175 }
18176 const ids = Array.isArray(detail.ids) ? detail.ids.length : 0;
18177 switch (detail.action) {
18178 case "trashed":
18179 adjustRecycleBinBadge(+ids);
18180 break;
18181 case "untrashed":
18182 case "deleted":
18183 adjustRecycleBinBadge(-ids);
18184 break;
18185 }
18186 };
18187 subscribe$2("desktop-mode.post.changed", onDomain);
18188 subscribe$2("desktop-mode.page.changed", onDomain);
18189 subscribe$2("desktop-mode.attachment.changed", onDomain);
18190 subscribe$2("desktop-mode.comment.changed", onDomain);
18191 subscribe$2("desktop-mode.placement.changed", onDomain);
18192 subscribe$2("desktop-mode.shortcut.changed", onDomain);
18193 subscribe$2("desktop-mode.folder.changed", onDomain);
18194 }
18195 function wirePostMessageFastPath() {
18196 const expectedOrigin = window.location.origin;
18197 window.addEventListener("message", (e) => {
18198 if (e.origin !== expectedOrigin) {
18199 return;
18200 }
18201 const data = e.data;
18202 if (!data || data.type !== "desktop-mode-recycle-bin-changed") {
18203 return;
18204 }
18205 const ts = typeof data.ts === "number" ? data.ts : Date.now();
18206 if (ts <= store$3.state.seenTs) {
18207 log("postMessage skipped (ts <= seenTs)", { ts, seenTs: store$3.state.seenTs });
18208 return;
18209 }
18210 log("postMessage triggers refetch", { ts, prevSeenTs: store$3.state.seenTs });
18211 store$3.state.seenTs = ts;
18212 void refetchCount();
18213 });
18214 }
18215 function wireHeartbeatProbe() {
18216 const $ = window.jQuery;
18217 if (!$) {
18218 warn("wireHeartbeatProbe: window.jQuery not available — heartbeat path disabled");
18219 return;
18220 }
18221 log("wireHeartbeatProbe: jQuery + heartbeat hooks attached");
18222 $(document).on("heartbeat-send", (...args) => {
18223 const data = args[1];
18224 if (data) {
18225 data[HEARTBEAT_FIELD$1] = store$3.state.seenTs;
18226 }
18227 });
18228 $(document).on("heartbeat-tick", (...args) => {
18229 const response = args[1];
18230 const block = response?.desktop_mode_recycle_bin;
18231 log("heartbeat-tick", { hasBlock: !!block, block });
18232 if (!block) {
18233 return;
18234 }
18235 if (typeof block.ts === "number" && block.ts > store$3.state.seenTs) {
18236 store$3.state.seenTs = block.ts;
18237 }
18238 if (typeof block.count === "number") {
18239 setRecycleBinBadge(block.count);
18240 }
18241 });
18242 }
18243 async function refetchCount() {
18244 if (!store$3.state.countUrl) {
18245 log("refetchCount: no countUrl, skip");
18246 return;
18247 }
18248 log("refetchCount: hitting", store$3.state.countUrl);
18249 try {
18250 const response = await fetch(store$3.state.countUrl, {
18251 credentials: "same-origin",
18252 headers: { Accept: "application/json" }
18253 });
18254 if (!response.ok) {
18255 warn("refetchCount: non-OK", response.status, response.statusText);
18256 return;
18257 }
18258 const json = await response.json();
18259 log("refetchCount: response", json);
18260 if (typeof json.count === "number") {
18261 setRecycleBinBadge(json.count);
18262 }
18263 } catch (err) {
18264 warn("refetchCount: fetch failed", err);
18265 }
18266 }
18267 const OS_SETTINGS_ID = "desktop-mode-os-settings";
18268 const RECYCLE_BIN_ID = "desktop-mode-recycle-bin";
18269 function registerBuiltInPeekRenderers(opts) {
18270 const wpHooks = getWpHooks();
18271 if (!wpHooks) {
18272 return;
18273 }
18274 wpHooks.addFilter(
18275 "desktop-mode.dock.peek-card-content",
18276 "desktop-mode/built-in-peek-renderers",
18277 (body, ctx) => {
18278 const context = ctx;
18279 const id = context.window.id;
18280 if (id === OS_SETTINGS_ID) {
18281 return renderOsSettings();
18282 }
18283 if (id === RECYCLE_BIN_ID) {
18284 return renderRecycleBin(context, opts.getRecycleBinCount);
18285 }
18286 return body;
18287 }
18288 );
18289 }
18290 function renderOsSettings(_ctx) {
18291 const root = document.createElement("span");
18292 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--os-settings";
18293 root.setAttribute("aria-hidden", "true");
18294 const hero = document.createElement("span");
18295 hero.className = "desktop-mode-dock-peek__os-hero dashicons dashicons-admin-generic";
18296 root.appendChild(hero);
18297 const subtitle = document.createElement("span");
18298 subtitle.className = "desktop-mode-dock-peek__os-subtitle";
18299 subtitle.textContent = __("System Preferences");
18300 root.appendChild(subtitle);
18301 const tabs = document.createElement("span");
18302 tabs.className = "desktop-mode-dock-peek__os-tabs";
18303 for (const cls of [
18304 "dashicons-art",
18305 "dashicons-admin-customizer",
18306 "dashicons-editor-help"
18307 ]) {
18308 const tab = document.createElement("span");
18309 tab.className = `desktop-mode-dock-peek__os-tab dashicons ${cls}`;
18310 tabs.appendChild(tab);
18311 }
18312 root.appendChild(tabs);
18313 return root;
18314 }
18315 function renderRecycleBin(_ctx, getCount) {
18316 const root = document.createElement("span");
18317 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--recycle-bin";
18318 root.setAttribute("aria-hidden", "true");
18319 const count = Math.max(0, Math.floor(getCount() || 0));
18320 root.dataset.empty = count === 0 ? "true" : "false";
18321 const stage = document.createElement("span");
18322 stage.className = "desktop-mode-dock-peek__bin-stage";
18323 const stack = document.createElement("span");
18324 stack.className = "desktop-mode-dock-peek__bin-stack";
18325 for (let i = 0; i < 3; i++) {
18326 const slip = document.createElement("span");
18327 slip.className = "desktop-mode-dock-peek__bin-slip";
18328 stack.appendChild(slip);
18329 }
18330 stage.appendChild(stack);
18331 const icon = document.createElement("span");
18332 icon.className = `desktop-mode-dock-peek__bin-icon dashicons ${count === 0 ? "dashicons-trash" : "dashicons-trash"}`;
18333 stage.appendChild(icon);
18334 root.appendChild(stage);
18335 const label = document.createElement("span");
18336 label.className = "desktop-mode-dock-peek__bin-label";
18337 if (count === 0) {
18338 label.textContent = __("Recycle Bin — empty");
18339 } else if (count === 1) {
18340 label.textContent = __("1 item");
18341 } else if (count > 99) {
18342 label.textContent = "99+ items";
18343 } else {
18344 label.textContent = `${count} items`;
18345 }
18346 root.appendChild(label);
18347 return root;
18348 }
18349 function getWpHooks() {
18350 const wp = window.wp;
18351 return wp?.hooks ?? null;
18352 }
18353 const BUG_REPORT_WINDOW_ID = "desktop-mode-bug-report";
18354 const REPO_OWNER = "WordPress";
18355 const REPO_NAME = "desktop-mode";
18356 const MAX_BODY_LENGTH = 6e3;
18357 function renderBugReport(body) {
18358 body.classList.add("desktop-mode-bug-report");
18359 body.replaceChildren();
18360 const form = document.createElement("form");
18361 form.className = "desktop-mode-bug-report__form";
18362 form.setAttribute("novalidate", "");
18363 const intro = document.createElement("p");
18364 intro.className = "desktop-mode-bug-report__intro";
18365 intro.textContent = __(
18366 "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."
18367 );
18368 form.appendChild(intro);
18369 form.appendChild(buildTypeField());
18370 form.appendChild(buildTextField("title", __("Title"), {
18371 placeholder: __("A short summary"),
18372 required: true
18373 }));
18374 form.appendChild(buildTextareaField("description", __("What happened? What did you expect?"), {
18375 placeholder: __("Describe the issue or the feature you have in mind."),
18376 rows: 5,
18377 required: true
18378 }));
18379 form.appendChild(buildTextareaField("steps", __("Steps to reproduce (bug only)"), {
18380 placeholder: __("One step per line"),
18381 rows: 4
18382 }));
18383 const meta = buildMetadataPreview();
18384 form.appendChild(meta);
18385 const actions = document.createElement("div");
18386 actions.className = "desktop-mode-bug-report__actions";
18387 const submit = document.createElement("button");
18388 submit.type = "submit";
18389 submit.className = "desktop-mode-bug-report__submit";
18390 submit.textContent = __("Open issue on GitHub");
18391 actions.appendChild(submit);
18392 const hint = document.createElement("span");
18393 hint.className = "desktop-mode-bug-report__hint";
18394 hint.textContent = __("You will review and submit on GitHub.");
18395 actions.appendChild(hint);
18396 form.appendChild(actions);
18397 form.addEventListener("submit", (e) => {
18398 e.preventDefault();
18399 const state2 = readFormState(form);
18400 if (!state2.title.trim() || !state2.description.trim()) {
18401 showInlineError(form, __("Title and description are both required."));
18402 return;
18403 }
18404 const url = buildGithubIssueUrl(state2);
18405 window.open(url, "_blank", "noopener");
18406 });
18407 body.appendChild(form);
18408 }
18409 function buildTypeField() {
18410 const wrap = document.createElement("div");
18411 wrap.className = "desktop-mode-bug-report__field desktop-mode-bug-report__field--type";
18412 const label = document.createElement("span");
18413 label.className = "desktop-mode-bug-report__label";
18414 label.textContent = __("Type");
18415 wrap.appendChild(label);
18416 const group = document.createElement("div");
18417 group.className = "desktop-mode-bug-report__radio-group";
18418 group.setAttribute("role", "radiogroup");
18419 const options = [
18420 { value: "bug", label: __("Bug"), checked: true },
18421 { value: "feature", label: __("Feature request") },
18422 { value: "question", label: __("Question") }
18423 ];
18424 for (const opt of options) {
18425 const radioLabel = document.createElement("label");
18426 radioLabel.className = "desktop-mode-bug-report__radio";
18427 const input = document.createElement("input");
18428 input.type = "radio";
18429 input.name = "type";
18430 input.value = opt.value;
18431 if (opt.checked) {
18432 input.checked = true;
18433 }
18434 radioLabel.appendChild(input);
18435 const text = document.createElement("span");
18436 text.textContent = opt.label;
18437 radioLabel.appendChild(text);
18438 group.appendChild(radioLabel);
18439 }
18440 wrap.appendChild(group);
18441 return wrap;
18442 }
18443 function buildTextField(name, labelText, opts = {}) {
18444 const wrap = document.createElement("div");
18445 wrap.className = "desktop-mode-bug-report__field";
18446 const label = document.createElement("label");
18447 label.className = "desktop-mode-bug-report__label";
18448 label.textContent = labelText;
18449 wrap.appendChild(label);
18450 const input = document.createElement("input");
18451 input.type = "text";
18452 input.name = name;
18453 input.className = "desktop-mode-bug-report__input";
18454 if (opts.placeholder) {
18455 input.placeholder = opts.placeholder;
18456 }
18457 if (opts.required) {
18458 input.setAttribute("aria-required", "true");
18459 }
18460 label.appendChild(input);
18461 return wrap;
18462 }
18463 function buildTextareaField(name, labelText, opts = {}) {
18464 const wrap = document.createElement("div");
18465 wrap.className = "desktop-mode-bug-report__field";
18466 const label = document.createElement("label");
18467 label.className = "desktop-mode-bug-report__label";
18468 label.textContent = labelText;
18469 wrap.appendChild(label);
18470 const textarea = document.createElement("textarea");
18471 textarea.name = name;
18472 textarea.className = "desktop-mode-bug-report__textarea";
18473 textarea.rows = opts.rows ?? 4;
18474 if (opts.placeholder) {
18475 textarea.placeholder = opts.placeholder;
18476 }
18477 if (opts.required) {
18478 textarea.setAttribute("aria-required", "true");
18479 }
18480 label.appendChild(textarea);
18481 return wrap;
18482 }
18483 function buildMetadataPreview() {
18484 const details = document.createElement("details");
18485 details.className = "desktop-mode-bug-report__metadata";
18486 const summary = document.createElement("summary");
18487 summary.textContent = __("Environment included with the report");
18488 details.appendChild(summary);
18489 const pre = document.createElement("pre");
18490 pre.className = "desktop-mode-bug-report__metadata-body";
18491 pre.textContent = formatMetadata(collectMetadata());
18492 details.appendChild(pre);
18493 return details;
18494 }
18495 function showInlineError(form, msg) {
18496 let banner = form.querySelector(".desktop-mode-bug-report__error");
18497 if (!banner) {
18498 banner = document.createElement("div");
18499 banner.className = "desktop-mode-bug-report__error";
18500 banner.setAttribute("role", "alert");
18501 form.prepend(banner);
18502 }
18503 banner.textContent = msg;
18504 }
18505 function readFormState(form) {
18506 const data = new FormData(form);
18507 return {
18508 type: data.get("type") ?? "bug",
18509 title: data.get("title") ?? "",
18510 description: data.get("description") ?? "",
18511 steps: data.get("steps") ?? ""
18512 };
18513 }
18514 function buildGithubIssueUrl(state2) {
18515 const labels = labelsForType(state2.type);
18516 const body = composeIssueBody(state2);
18517 const params = new URLSearchParams();
18518 params.set("title", state2.title.trim());
18519 params.set("body", body);
18520 if (labels.length) {
18521 params.set("labels", labels.join(","));
18522 }
18523 return `https://github.com/${REPO_OWNER}/${REPO_NAME}/issues/new?${params.toString()}`;
18524 }
18525 function labelsForType(type) {
18526 switch (type) {
18527 case "bug":
18528 return ["bug"];
18529 case "feature":
18530 return ["enhancement"];
18531 case "question":
18532 return ["question"];
18533 default:
18534 return [];
18535 }
18536 }
18537 function composeIssueBody(state2) {
18538 const parts = [];
18539 parts.push(state2.description.trim());
18540 if (state2.type === "bug" && state2.steps.trim()) {
18541 parts.push("");
18542 parts.push("## Steps to reproduce");
18543 parts.push("");
18544 parts.push(state2.steps.trim());
18545 }
18546 parts.push("");
18547 parts.push("<details><summary>Environment</summary>");
18548 parts.push("");
18549 parts.push("```");
18550 parts.push(formatMetadata(collectMetadata()));
18551 parts.push("```");
18552 parts.push("");
18553 parts.push("</details>");
18554 let out = parts.join("\n");
18555 if (out.length > MAX_BODY_LENGTH) {
18556 out = out.slice(0, MAX_BODY_LENGTH) + "\n\n…(truncated to fit GitHub URL length limit)";
18557 }
18558 return out;
18559 }
18560 function collectMetadata() {
18561 const cfg = window.wp?.desktop?.config;
18562 return {
18563 pluginVersion: cfg?.pluginVersion ?? "unknown",
18564 wordpressVersion: cfg?.wordpressVersion ?? "unknown",
18565 userAgent: navigator.userAgent,
18566 viewport: `${window.innerWidth}x${window.innerHeight}`,
18567 platform: navigator.platform || "unknown",
18568 currentUrl: window.location.href
18569 };
18570 }
18571 function formatMetadata(m) {
18572 return [
18573 `Plugin version: ${m.pluginVersion}`,
18574 `WordPress version: ${m.wordpressVersion}`,
18575 `User agent: ${m.userAgent}`,
18576 `Viewport: ${m.viewport}`,
18577 `Platform: ${m.platform}`,
18578 `Current URL: ${m.currentUrl}`
18579 ].join("\n");
18580 }
18581 let _registration = null;
18582 let _registrationFailed = false;
18583 let _controllerChangeBound = false;
18584 let _reloadingForSwUpdate = false;
18585 let _status = "pending";
18586 function bindControllerChangeReload() {
18587 if (_controllerChangeBound) {
18588 return;
18589 }
18590 _controllerChangeBound = true;
18591 const hadInitialController = !!navigator.serviceWorker.controller;
18592 navigator.serviceWorker.addEventListener("controllerchange", () => {
18593 if (!hadInitialController) {
18594 return;
18595 }
18596 if (_reloadingForSwUpdate) {
18597 return;
18598 }
18599 if (wasRecentlyReloadedForSwUpdate()) {
18600 return;
18601 }
18602 markReloadedForSwUpdate();
18603 _reloadingForSwUpdate = true;
18604 setTimeout(() => window.location.reload(), 0);
18605 });
18606 }
18607 const SW_RELOAD_THROTTLE_KEY = "wpd-sw-reload-ts";
18608 const SW_RELOAD_THROTTLE_MS = 3e4;
18609 function wasRecentlyReloadedForSwUpdate() {
18610 try {
18611 const raw = sessionStorage.getItem(SW_RELOAD_THROTTLE_KEY);
18612 const last = raw ? Number.parseInt(raw, 10) : 0;
18613 if (!Number.isFinite(last) || last <= 0) {
18614 return false;
18615 }
18616 return Date.now() - last < SW_RELOAD_THROTTLE_MS;
18617 } catch {
18618 return false;
18619 }
18620 }
18621 function markReloadedForSwUpdate() {
18622 try {
18623 sessionStorage.setItem(SW_RELOAD_THROTTLE_KEY, String(Date.now()));
18624 } catch {
18625 }
18626 }
18627 async function registerServiceWorker(config, options = {}) {
18628 if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
18629 _status = "unsupported";
18630 return null;
18631 }
18632 if (!config?.swUrl) {
18633 _status = "unsupported";
18634 return null;
18635 }
18636 if (!window.isSecureContext) {
18637 _status = "unsupported";
18638 return null;
18639 }
18640 if (_registration || _registrationFailed) {
18641 return _registration;
18642 }
18643 if (!options.forceReplace) {
18644 const existing = await navigator.serviceWorker.getRegistrations().catch(() => []);
18645 const foreign = existing.find((reg) => {
18646 const url = reg.active?.scriptURL ?? reg.installing?.scriptURL ?? "";
18647 return url !== "" && url !== config.swUrl;
18648 });
18649 if (foreign) {
18650 _status = "foreign-sw";
18651 if (typeof console !== "undefined") {
18652 console.warn(
18653 "[desktop-mode] another service worker is already registered (" + foreign.scope + "); skipping desktop-mode SW. Set desktop_mode_pwa_force_replace_sw=true to override."
18654 );
18655 }
18656 return null;
18657 }
18658 }
18659 try {
18660 _registration = await navigator.serviceWorker.register(config.swUrl, {
18661 scope: "/",
18662 updateViaCache: "none"
18663 });
18664 _status = "registered";
18665 bindControllerChangeReload();
18666 return _registration;
18667 } catch (err) {
18668 _registrationFailed = true;
18669 _status = "failed";
18670 if (typeof console !== "undefined") {
18671 console.warn("[desktop-mode] SW registration failed:", err);
18672 }
18673 return null;
18674 }
18675 }
18676 function getSwRegistrationStatus() {
18677 return _status;
18678 }
18679 const PWA_INSTALL_TILE_ID = "desktop-mode-pwa-install";
18680 function isStandaloneDisplay() {
18681 if (typeof window === "undefined") {
18682 return false;
18683 }
18684 if (window.matchMedia?.("(display-mode: standalone)").matches) {
18685 return true;
18686 }
18687 const nav = window.navigator;
18688 return nav.standalone === true;
18689 }
18690 async function isLikelyInstalled() {
18691 if (isStandaloneDisplay()) {
18692 return true;
18693 }
18694 const nav = window.navigator;
18695 if (typeof nav.getInstalledRelatedApps !== "function") {
18696 return false;
18697 }
18698 try {
18699 const apps = await nav.getInstalledRelatedApps();
18700 return Array.isArray(apps) && apps.length > 0;
18701 } catch {
18702 return false;
18703 }
18704 }
18705 let _deferred = null;
18706 function installPwaInstallAffordance(siteName, showToast2) {
18707 if (typeof window === "undefined") {
18708 return;
18709 }
18710 window.removeEventListener(
18711 "beforeinstallprompt",
18712 _handleBeforeInstall
18713 );
18714 window.addEventListener(
18715 "beforeinstallprompt",
18716 _handleBeforeInstall
18717 );
18718 window.removeEventListener("appinstalled", _handleAppInstalled);
18719 window.addEventListener("appinstalled", _handleAppInstalled);
18720 function _handleBeforeInstall(ev) {
18721 ev.preventDefault();
18722 _deferred = ev;
18723 }
18724 function _handleAppInstalled() {
18725 _deferred = null;
18726 showToast2({
18727 message: sprintf(
18728 /* translators: %s: site name */
18729 __("Installed %s as an app."),
18730 siteName
18731 )
18732 });
18733 }
18734 }
18735 function getInstallTileDef(siteName, showToast2) {
18736 return {
18737 id: PWA_INSTALL_TILE_ID,
18738 title: sprintf(
18739 /* translators: %s: site name */
18740 __("Install %s as an app"),
18741 siteName
18742 ),
18743 // Dashicons class — the dock renderer prefers Dashicons
18744 // strings. `dashicons-download` is the closest match for
18745 // "install" in the WordPress glyph set without shipping
18746 // bespoke artwork.
18747 icon: "dashicons-download",
18748 onOpen: () => {
18749 void onTileClick(siteName, showToast2);
18750 }
18751 };
18752 }
18753 async function onTileClick(siteName, showToast2) {
18754 if (_deferred) {
18755 const event = _deferred;
18756 _deferred = null;
18757 try {
18758 await event.prompt();
18759 const choice = await event.userChoice;
18760 if (choice.outcome === "dismissed") {
18761 showToast2({
18762 message: __("Install cancelled.")
18763 });
18764 }
18765 } catch (err) {
18766 if (typeof console !== "undefined") {
18767 console.warn(
18768 "[desktop-mode] install prompt failed:",
18769 err
18770 );
18771 }
18772 }
18773 return;
18774 }
18775 if (await isLikelyInstalled()) {
18776 showToast2({
18777 message: sprintf(
18778 /* translators: %s: site name */
18779 __(
18780 "%s is already installed. Open it from your apps menu or home screen."
18781 ),
18782 siteName
18783 )
18784 });
18785 return;
18786 }
18787 if (getSwRegistrationStatus() === "foreign-sw") {
18788 showToast2({
18789 message: __(
18790 "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."
18791 )
18792 });
18793 return;
18794 }
18795 showToast2({
18796 message: __(
18797 "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."
18798 )
18799 });
18800 }
18801 async function promptInstall() {
18802 if (!_deferred) {
18803 return "unavailable";
18804 }
18805 const event = _deferred;
18806 _deferred = null;
18807 try {
18808 await event.prompt();
18809 const choice = await event.userChoice;
18810 return choice.outcome;
18811 } catch {
18812 return "unavailable";
18813 }
18814 }
18815 function undismissInstallHint() {
18816 Promise.resolve().then(() => state).then((m) => {
18817 m.updatePwaState({ installHintDismissed: false });
18818 });
18819 }
18820 function bootstrapPwa(config, showToast2) {
18821 if (!config.pwa) {
18822 return;
18823 }
18824 initPwaState(config.pwa);
18825 installPwaInstallAffordance(
18826 config.pwa.appName || "WordPress",
18827 showToast2
18828 );
18829 void registerServiceWorker(config.pwa, {
18830 forceReplace: !!config.pwa.forceReplaceSw
18831 });
18832 }
18833 const DRAG_BRIDGE_EVENTS = {
18834 START: "desktop-mode-cross-frame-drag-start",
18835 END: "desktop-mode-cross-frame-drag-end"
18836 };
18837 function isStart(m) {
18838 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-start" && !!m.payload && typeof m.payload === "object";
18839 }
18840 function isEnd(m) {
18841 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-end";
18842 }
18843 function isPayloadRequest(m) {
18844 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-payload-request";
18845 }
18846 function normalizeLegacyPayload(payload) {
18847 const obj = payload;
18848 if (obj.kind !== void 0 && obj.kind !== null) {
18849 return payload;
18850 }
18851 if (typeof obj.id === "number" && typeof obj.url === "string" && typeof obj.mime === "string") {
18852 return {
18853 kind: "attachment",
18854 id: obj.id,
18855 url: obj.url,
18856 title: typeof obj.title === "string" ? obj.title : "",
18857 alt: typeof obj.alt === "string" ? obj.alt : "",
18858 mime: obj.mime,
18859 thumbnailUrl: typeof obj.thumbnailUrl === "string" ? obj.thumbnailUrl : void 0,
18860 sizes: obj.sizes && typeof obj.sizes === "object" ? obj.sizes : void 0
18861 };
18862 }
18863 return payload;
18864 }
18865 class DragBridge {
18866 constructor() {
18867 this._payload = null;
18868 this._onMessage = (e) => {
18869 if (e.origin !== this._origin) {
18870 return;
18871 }
18872 const msg = e.data;
18873 if (isStart(msg)) {
18874 this._startDrag(msg.payload);
18875 return;
18876 }
18877 if (isEnd(msg)) {
18878 this._endDrag();
18879 return;
18880 }
18881 if (isPayloadRequest(msg) && this._payload && e.source) {
18882 try {
18883 e.source.postMessage(
18884 { type: "desktop-mode-drag-payload", payload: this._payload },
18885 this._origin
18886 );
18887 } catch {
18888 }
18889 }
18890 };
18891 this._origin = window.location.origin;
18892 window.addEventListener("message", this._onMessage);
18893 }
18894 getPayload() {
18895 return this._payload;
18896 }
18897 isDragging() {
18898 return this._payload !== null;
18899 }
18900 start(payload) {
18901 if (this._payload === payload) {
18902 return;
18903 }
18904 this._startDrag(payload);
18905 }
18906 end() {
18907 this._endDrag();
18908 }
18909 _startDrag(payload) {
18910 const normalized = normalizeLegacyPayload(payload);
18911 this._payload = normalized;
18912 document.dispatchEvent(
18913 new CustomEvent(DRAG_BRIDGE_EVENTS.START, {
18914 detail: { payload: normalized }
18915 })
18916 );
18917 }
18918 _endDrag() {
18919 if (this._payload === null) {
18920 return;
18921 }
18922 const payload = this._payload;
18923 this._payload = null;
18924 document.dispatchEvent(
18925 new CustomEvent(DRAG_BRIDGE_EVENTS.END, { detail: { payload } })
18926 );
18927 }
18928 }
18929 class DropTargetRegistry {
18930 constructor() {
18931 this._targets = /* @__PURE__ */ new Map();
18932 this._byElement = /* @__PURE__ */ new Map();
18933 }
18934 register(target2) {
18935 const prev = this._targets.get(target2.id);
18936 if (prev) {
18937 this._byElement.delete(prev.element);
18938 }
18939 this._targets.set(target2.id, target2);
18940 this._byElement.set(target2.element, target2);
18941 return () => {
18942 const cur = this._targets.get(target2.id);
18943 if (cur === target2) {
18944 this._targets.delete(target2.id);
18945 this._byElement.delete(target2.element);
18946 }
18947 };
18948 }
18949 list() {
18950 return Array.from(this._targets.values());
18951 }
18952 clear() {
18953 this._targets.clear();
18954 this._byElement.clear();
18955 }
18956 /**
18957 * Find the deepest registered target whose element is `el` or an
18958 * ancestor of `el`. Walks the DOM tree once (O(depth)).
18959 *
18960 * Window claim boundary: if the walk crosses a `.desktop-mode-window`
18961 * element BEFORE finding a registered target, hit-testing stops
18962 * there and returns null. This is the rule that makes "drag over
18963 * a Gutenberg admin window" produce reject feedback instead of
18964 * silently routing the drop to the wallpaper canvas underneath.
18965 *
18966 * A window can opt INTO accepting drops by registering a target
18967 * on its own body (e.g. Recycle Bin's `[data-desktop-mode-recycle-bin-root]`):
18968 * since that element sits inside the window, the walk hits it
18969 * before reaching the window boundary and the body's target wins.
18970 */
18971 hitTest(el) {
18972 let cur = el;
18973 while (cur) {
18974 if (cur instanceof HTMLElement) {
18975 const t = this._byElement.get(cur);
18976 if (t) {
18977 return t;
18978 }
18979 if (cur.classList.contains("desktop-mode-window")) {
18980 return null;
18981 }
18982 }
18983 cur = cur.parentElement;
18984 }
18985 return null;
18986 }
18987 /**
18988 * Convenience: pick the target at viewport `(clientX, clientY)`.
18989 * Caller is responsible for hiding any obscuring ghost element
18990 * before calling — see `GhostHandle.withHidden()`.
18991 */
18992 hitTestPoint(clientX, clientY) {
18993 const el = document.elementFromPoint(clientX, clientY);
18994 const target2 = this.hitTest(el);
18995 return { target: target2, element: el, accepted: false };
18996 }
18997 }
18998 const GHOST_CLASS = "desktop-mode-drag-ghost";
18999 const GHOST_ACCEPT_CLASS = "desktop-mode-drag-ghost--accept";
19000 const GHOST_REJECT_CLASS = "desktop-mode-drag-ghost--reject";
19001 const HINT_CLASS = "desktop-mode-drag-hint";
19002 const HINT_ACCEPT_CLASS = "desktop-mode-drag-hint--accept";
19003 const HINT_REJECT_CLASS = "desktop-mode-drag-hint--reject";
19004 const HINT_NEUTRAL_CLASS = "desktop-mode-drag-hint--neutral";
19005 const HINT_OFFSET_X = 16;
19006 const HINT_OFFSET_Y = 18;
19007 function mountGhost(payload, clientX, clientY) {
19008 const ghost = buildGhost(payload);
19009 const offsetX = payload.ghost?.offsetX ?? defaultOffsetX(payload.source);
19010 const offsetY = payload.ghost?.offsetY ?? defaultOffsetY(payload.source);
19011 ghost.classList.add(GHOST_CLASS);
19012 ghost.setAttribute("aria-hidden", "true");
19013 ghost.style.position = "fixed";
19014 ghost.style.left = "0";
19015 ghost.style.top = "0";
19016 ghost.style.margin = "0";
19017 ghost.style.pointerEvents = "none";
19018 ghost.style.zIndex = "2147483647";
19019 ghost.style.willChange = "transform";
19020 document.body.appendChild(ghost);
19021 const labels = resolveHintLabels(payload);
19022 const hint = labels ? buildHintChip() : null;
19023 if (hint) {
19024 document.body.appendChild(hint);
19025 }
19026 const handle = {
19027 get element() {
19028 return ghost;
19029 },
19030 moveTo(cx, cy) {
19031 ghost.style.transform = `translate3d(${cx - offsetX}px, ${cy - offsetY}px, 0)`;
19032 if (hint) {
19033 hint.style.transform = `translate3d(${cx + HINT_OFFSET_X}px, ${cy + HINT_OFFSET_Y}px, 0)`;
19034 }
19035 },
19036 setMode(mode, overrides) {
19037 ghost.classList.remove(GHOST_ACCEPT_CLASS, GHOST_REJECT_CLASS);
19038 if (mode === "accept") {
19039 ghost.classList.add(GHOST_ACCEPT_CLASS);
19040 } else if (mode === "reject") {
19041 ghost.classList.add(GHOST_REJECT_CLASS);
19042 }
19043 if (hint && labels) {
19044 hint.classList.remove(
19045 HINT_ACCEPT_CLASS,
19046 HINT_REJECT_CLASS,
19047 HINT_NEUTRAL_CLASS
19048 );
19049 if (mode === "accept") {
19050 hint.classList.add(HINT_ACCEPT_CLASS);
19051 hint.textContent = overrides?.acceptLabel ?? labels.accept;
19052 } else if (mode === "reject") {
19053 hint.classList.add(HINT_REJECT_CLASS);
19054 hint.textContent = labels.reject;
19055 } else {
19056 hint.classList.add(HINT_NEUTRAL_CLASS);
19057 hint.textContent = labels.neutral;
19058 }
19059 hint.hidden = !hint.textContent;
19060 }
19061 },
19062 withHidden(fn) {
19063 const prevG = ghost.style.visibility;
19064 const prevH = hint?.style.visibility ?? "";
19065 ghost.style.visibility = "hidden";
19066 if (hint) {
19067 hint.style.visibility = "hidden";
19068 }
19069 try {
19070 return fn();
19071 } finally {
19072 ghost.style.visibility = prevG;
19073 if (hint) {
19074 hint.style.visibility = prevH;
19075 }
19076 }
19077 },
19078 dispose() {
19079 if (ghost.isConnected) {
19080 ghost.remove();
19081 }
19082 if (hint?.isConnected) {
19083 hint.remove();
19084 }
19085 }
19086 };
19087 handle.moveTo(clientX, clientY);
19088 handle.setMode("neutral");
19089 return handle;
19090 }
19091 function buildHintChip() {
19092 const chip = document.createElement("div");
19093 chip.className = HINT_CLASS;
19094 chip.setAttribute("aria-hidden", "true");
19095 chip.setAttribute("role", "presentation");
19096 chip.style.position = "fixed";
19097 chip.style.left = "0";
19098 chip.style.top = "0";
19099 chip.style.margin = "0";
19100 chip.style.pointerEvents = "none";
19101 chip.style.zIndex = "2147483647";
19102 chip.style.willChange = "transform";
19103 return chip;
19104 }
19105 function resolveHintLabels(payload) {
19106 const cfg = payload.ghost?.hint;
19107 if (cfg?.hidden) {
19108 return null;
19109 }
19110 return {
19111 accept: cfg?.accept ?? defaultAcceptLabel(payload),
19112 reject: cfg?.reject ?? defaultRejectLabel(),
19113 neutral: cfg?.neutral ?? defaultNeutralLabel(payload)
19114 };
19115 }
19116 function defaultAcceptLabel(payload) {
19117 if (payload.type === "shortcut") {
19118 return __("Drop here to create shortcut", "desktop-mode");
19119 }
19120 if (payload.type === "desktop-file") {
19121 return __("Drop here to move", "desktop-mode");
19122 }
19123 return __("Drop here", "desktop-mode");
19124 }
19125 function defaultRejectLabel(_payload) {
19126 return __("Can’t drop here", "desktop-mode");
19127 }
19128 function defaultNeutralLabel(payload) {
19129 if (payload.type === "shortcut") {
19130 return __(
19131 "Drop on the desktop or a folder",
19132 "desktop-mode"
19133 );
19134 }
19135 if (payload.type === "desktop-file") {
19136 return __("Drop in a folder", "desktop-mode");
19137 }
19138 return "";
19139 }
19140 function buildGhost(payload) {
19141 if (payload.ghost?.element) {
19142 return payload.ghost.element;
19143 }
19144 const clone = payload.source.cloneNode(true);
19145 clone.removeAttribute("id");
19146 const rect = payload.source.getBoundingClientRect();
19147 clone.style.width = `${rect.width}px`;
19148 clone.style.height = `${rect.height}px`;
19149 return clone;
19150 }
19151 function defaultOffsetX(source) {
19152 return source.offsetWidth / 2;
19153 }
19154 function defaultOffsetY(source) {
19155 return source.offsetHeight / 2;
19156 }
19157 let _installed$4 = false;
19158 function installRecovery(cancelActive) {
19159 if (_installed$4) {
19160 return;
19161 }
19162 _installed$4 = true;
19163 document.addEventListener("keydown", (e) => {
19164 if (e.key === "Escape") {
19165 cancelActive("escape");
19166 }
19167 });
19168 window.addEventListener("blur", () => {
19169 cancelActive("blur");
19170 });
19171 document.addEventListener("visibilitychange", () => {
19172 if (document.hidden) {
19173 cancelActive("visibility");
19174 }
19175 });
19176 }
19177 const DRAG_THRESHOLD_PX = 4;
19178 const DRAG_EVENTS = {
19179 START: "desktop-mode.drag.start",
19180 MOVE: "desktop-mode.drag.move",
19181 ENTER: "desktop-mode.drag.enter",
19182 LEAVE: "desktop-mode.drag.leave",
19183 REJECTED: "desktop-mode.drag.rejected",
19184 COMMIT: "desktop-mode.drag.commit",
19185 CANCEL: "desktop-mode.drag.cancel",
19186 END: "desktop-mode.drag.end"
19187 };
19188 const SOURCE_DRAGGING_CLASS = "desktop-mode-file-tile--dragging";
19189 const TARGET_DROP_ACTIVE_CLASS = "desktop-mode-file-tile--drop-target";
19190 const TRASH_DROP_ACTIVE_ATTR$1 = "data-desktop-mode-trash-drop-active";
19191 const FILES_DROP_ACTIVE_ATTR = "data-files-drop-active";
19192 const BODY_DRAGGING_ATTR = "data-desktop-mode-dragging";
19193 const BODY_DRAG_TYPE_ATTR = "data-desktop-mode-drag-type";
19194 const BODY_DRAG_MODE_ATTR = "data-desktop-mode-drag-mode";
19195 class DragManager {
19196 constructor() {
19197 this._registry = new DropTargetRegistry();
19198 this._active = null;
19199 this._docListenersAttached = false;
19200 this._lastLiftedEndAt = 0;
19201 this._onPointerMove = (e) => {
19202 const session = this._active;
19203 if (!session || session._pointerId !== e.pointerId) {
19204 return;
19205 }
19206 const dx = e.clientX - session._origin.clientX;
19207 const dy = e.clientY - session._origin.clientY;
19208 if (!session._lifted) {
19209 if (Math.abs(dx) < DRAG_THRESHOLD_PX && Math.abs(dy) < DRAG_THRESHOLD_PX) {
19210 return;
19211 }
19212 this._lift(session, e);
19213 }
19214 if (!session._ghost) {
19215 return;
19216 }
19217 session._ghost.moveTo(e.clientX, e.clientY);
19218 this._updateHover(session, e.clientX, e.clientY);
19219 dispatchOnDocument(DRAG_EVENTS.MOVE, {
19220 payload: session.payload,
19221 clientX: e.clientX,
19222 clientY: e.clientY
19223 });
19224 };
19225 this._onPointerUp = (e) => {
19226 const session = this._active;
19227 if (!session || session._pointerId !== e.pointerId) {
19228 return;
19229 }
19230 if (!session._lifted) {
19231 session._finished = true;
19232 this._active = null;
19233 try {
19234 session._callbacks.onClickOnly?.();
19235 } catch (err) {
19236 console.error("[desktop-mode] drag onClickOnly threw:", err);
19237 }
19238 return;
19239 }
19240 const hit = this._hitTestNow(session, e.clientX, e.clientY);
19241 if (hit && hit.accepted && hit.target) {
19242 this._commit(session, hit.target, e.clientX, e.clientY);
19243 return;
19244 }
19245 this._cancel(session, hit && hit.target ? "rejected" : "no-target");
19246 };
19247 this._onPointerCancel = (e) => {
19248 const session = this._active;
19249 if (!session || session._pointerId !== e.pointerId) {
19250 return;
19251 }
19252 this._cancel(session, "pointercancel");
19253 };
19254 }
19255 start(opts) {
19256 if (this._active) {
19257 return null;
19258 }
19259 if (opts.origin.button !== 0) {
19260 return null;
19261 }
19262 const session = {
19263 payload: opts.payload,
19264 isFinished: () => session._finished,
19265 cancel: (reason) => this._cancel(session, reason ?? "caller"),
19266 _origin: opts.origin,
19267 _pointerId: opts.origin.pointerId,
19268 _lifted: false,
19269 _finished: false,
19270 _callbacks: {
19271 onClickOnly: opts.onClickOnly,
19272 onCancel: opts.onCancel,
19273 onCommit: opts.onCommit
19274 },
19275 _ghost: null,
19276 _currentTarget: null,
19277 _currentAccepted: false
19278 };
19279 this._active = session;
19280 this._ensureDocListeners();
19281 installRecovery((reason) => {
19282 if (this._active) {
19283 this._cancel(this._active, reason);
19284 }
19285 });
19286 return session;
19287 }
19288 registerDropTarget(target2) {
19289 return this._registry.register(target2);
19290 }
19291 isDragging() {
19292 return this._active !== null && this._active._lifted;
19293 }
19294 /**
19295 * Whether a real (lifted) drag ended within `withinMs` of now.
19296 * Surfaces that bind plain `click` listeners use this to ignore
19297 * the synthesized click that fires after a drop. 500 ms is a
19298 * generous default — browsers fire the click within 10–50 ms of
19299 * pointerup, but plugins may chain post-drag work into a
19300 * `requestAnimationFrame` and call back into a click-driven API.
19301 *
19302 * @public
19303 * @since 0.8.5
19304 */
19305 recentlyEndedDrag(withinMs = 500) {
19306 if (this._lastLiftedEndAt === 0) {
19307 return false;
19308 }
19309 return Date.now() - this._lastLiftedEndAt < withinMs;
19310 }
19311 getActive() {
19312 return this._active;
19313 }
19314 debug() {
19315 return {
19316 findOrphans: () => findOrphans(),
19317 listTargets: () => this._registry.list()
19318 };
19319 }
19320 // -----------------------------------------------------------------
19321 // Internals
19322 // -----------------------------------------------------------------
19323 _ensureDocListeners() {
19324 if (this._docListenersAttached) {
19325 return;
19326 }
19327 this._docListenersAttached = true;
19328 document.addEventListener("pointermove", this._onPointerMove, true);
19329 document.addEventListener("pointerup", this._onPointerUp, true);
19330 document.addEventListener("pointercancel", this._onPointerCancel, true);
19331 }
19332 _lift(session, e) {
19333 session._lifted = true;
19334 session.payload.source.classList.add(SOURCE_DRAGGING_CLASS);
19335 session._ghost = mountGhost(session.payload, e.clientX, e.clientY);
19336 if (typeof document !== "undefined" && document.body) {
19337 document.body.setAttribute(BODY_DRAGGING_ATTR, "");
19338 document.body.setAttribute(
19339 BODY_DRAG_TYPE_ATTR,
19340 String(session.payload.type)
19341 );
19342 document.body.setAttribute(BODY_DRAG_MODE_ATTR, "neutral");
19343 }
19344 dispatchOnDocument(DRAG_EVENTS.START, { payload: session.payload });
19345 }
19346 _hitTestNow(session, clientX, clientY) {
19347 const run = () => {
19348 const el = document.elementFromPoint(clientX, clientY);
19349 const target2 = this._registry.hitTest(el);
19350 if (!target2) {
19351 return { target: null, accepted: false };
19352 }
19353 let accepted = false;
19354 try {
19355 accepted = target2.accept(session.payload);
19356 } catch (err) {
19357 console.error("[desktop-mode] drop target accept() threw:", target2.id, err);
19358 }
19359 return { target: target2, accepted };
19360 };
19361 if (session._ghost) {
19362 return session._ghost.withHidden(run);
19363 }
19364 return run();
19365 }
19366 _updateHover(session, clientX, clientY) {
19367 const next = this._hitTestNow(session, clientX, clientY);
19368 const prevTarget = session._currentTarget;
19369 if (next.target === prevTarget && next.accepted === session._currentAccepted) {
19370 return;
19371 }
19372 if (prevTarget) {
19373 fireLeave(prevTarget, session);
19374 }
19375 session._currentTarget = next.target;
19376 session._currentAccepted = next.accepted;
19377 let mode;
19378 if (next.target) {
19379 if (next.accepted) {
19380 fireEnter(next.target, session);
19381 session._ghost?.setMode("accept", {
19382 acceptLabel: next.target.acceptLabel
19383 });
19384 mode = "accept";
19385 } else {
19386 session._ghost?.setMode("reject");
19387 dispatchOnDocument(DRAG_EVENTS.REJECTED, {
19388 payload: session.payload,
19389 targetId: next.target.id
19390 });
19391 mode = "reject";
19392 }
19393 } else {
19394 session._ghost?.setMode("reject");
19395 mode = "reject";
19396 }
19397 if (typeof document !== "undefined" && document.body) {
19398 document.body.setAttribute(BODY_DRAG_MODE_ATTR, mode);
19399 }
19400 }
19401 _commit(session, target2, clientX, clientY) {
19402 session._finished = true;
19403 this._lastLiftedEndAt = Date.now();
19404 fireLeave(target2, session);
19405 this._cleanupDom(session);
19406 const prevActive = this._active;
19407 this._active = null;
19408 try {
19409 void target2.onDrop(session, { clientX, clientY });
19410 } catch (err) {
19411 console.error("[desktop-mode] drop target onDrop threw:", target2.id, err);
19412 }
19413 try {
19414 session._callbacks.onCommit?.(target2);
19415 } catch (err) {
19416 console.error("[desktop-mode] drag onCommit threw:", err);
19417 }
19418 dispatchOnDocument(DRAG_EVENTS.COMMIT, {
19419 payload: session.payload,
19420 targetId: target2.id
19421 });
19422 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason: "commit" });
19423 if (this._active === prevActive) {
19424 this._active = null;
19425 }
19426 }
19427 _cancel(session, reason) {
19428 if (session._finished) {
19429 return;
19430 }
19431 session._finished = true;
19432 if (session._lifted) {
19433 this._lastLiftedEndAt = Date.now();
19434 }
19435 if (session._currentTarget) {
19436 fireLeave(session._currentTarget, session);
19437 }
19438 this._cleanupDom(session);
19439 this._active = null;
19440 try {
19441 session._callbacks.onCancel?.(reason);
19442 } catch (err) {
19443 console.error("[desktop-mode] drag onCancel threw:", err);
19444 }
19445 dispatchOnDocument(DRAG_EVENTS.CANCEL, { payload: session.payload, reason });
19446 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason });
19447 }
19448 _cleanupDom(session) {
19449 try {
19450 session.payload.source.classList.remove(SOURCE_DRAGGING_CLASS);
19451 } catch {
19452 }
19453 session._ghost?.dispose();
19454 session._ghost = null;
19455 session._currentTarget = null;
19456 session._currentAccepted = false;
19457 if (typeof document !== "undefined" && document.body) {
19458 document.body.removeAttribute(BODY_DRAGGING_ATTR);
19459 document.body.removeAttribute(BODY_DRAG_TYPE_ATTR);
19460 document.body.removeAttribute(BODY_DRAG_MODE_ATTR);
19461 }
19462 scrubOrphans();
19463 }
19464 }
19465 function dispatchOnDocument(type, detail) {
19466 if (typeof document === "undefined") {
19467 return;
19468 }
19469 document.dispatchEvent(new CustomEvent(type, { detail }));
19470 }
19471 function fireEnter(target2, session) {
19472 try {
19473 target2.onEnter?.(session);
19474 } catch (err) {
19475 console.error("[desktop-mode] drop target onEnter threw:", target2.id, err);
19476 }
19477 dispatchOnDocument(DRAG_EVENTS.ENTER, {
19478 payload: session.payload,
19479 targetId: target2.id
19480 });
19481 }
19482 function fireLeave(target2, session) {
19483 try {
19484 target2.onLeave?.(session);
19485 } catch (err) {
19486 console.error("[desktop-mode] drop target onLeave threw:", target2.id, err);
19487 }
19488 dispatchOnDocument(DRAG_EVENTS.LEAVE, {
19489 payload: session.payload,
19490 targetId: target2.id
19491 });
19492 }
19493 function findOrphans() {
19494 if (typeof document === "undefined") {
19495 return [];
19496 }
19497 const out = [];
19498 for (const sel of [
19499 `.${SOURCE_DRAGGING_CLASS}`,
19500 `.${TARGET_DROP_ACTIVE_CLASS}`,
19501 `[${TRASH_DROP_ACTIVE_ATTR$1}]`,
19502 `[${FILES_DROP_ACTIVE_ATTR}]`
19503 ]) {
19504 document.querySelectorAll(sel).forEach((el) => out.push(el));
19505 }
19506 return out;
19507 }
19508 function scrubOrphans() {
19509 for (const el of findOrphans()) {
19510 el.classList.remove(SOURCE_DRAGGING_CLASS, TARGET_DROP_ACTIVE_CLASS);
19511 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR$1);
19512 el.removeAttribute(FILES_DROP_ACTIVE_ATTR);
19513 }
19514 }
19515 const WINDOW_ROOT_SELECTOR = ".desktop-mode-window";
19516 const WINDOW_ID_PREFIX = "wp-window-";
19517 function findWindowRootAtPoint(clientX, clientY) {
19518 const el = document.elementFromPoint(clientX, clientY);
19519 if (!el) {
19520 return null;
19521 }
19522 const root = el.closest(WINDOW_ROOT_SELECTOR);
19523 return root instanceof HTMLElement ? root : null;
19524 }
19525 function windowIdFromRoot(root) {
19526 if (!root.id.startsWith(WINDOW_ID_PREFIX)) {
19527 return null;
19528 }
19529 const id = root.id.slice(WINDOW_ID_PREFIX.length);
19530 return id.length > 0 ? id : null;
19531 }
19532 const TARGET_ID_PREFIX = "desktop-mode-iframe-drop-";
19533 const IFRAME_SELECTOR = "iframe.desktop-mode-window__iframe";
19534 const DROP_ACTIVE_ATTR$1 = "data-desktop-mode-iframe-drop-active";
19535 let _installed$3 = false;
19536 let _dragManager = null;
19537 const _suppressedIframes = /* @__PURE__ */ new Map();
19538 const _activeRegistrations = /* @__PURE__ */ new Map();
19539 let _bridgeInterceptPayload = null;
19540 let _lastHoveredBridgeIframe = null;
19541 function suppressIframePointerEventsBridge() {
19542 const iframes = document.querySelectorAll(
19543 IFRAME_SELECTOR
19544 );
19545 iframes.forEach((iframe) => {
19546 if (_suppressedIframes.has(iframe)) {
19547 return;
19548 }
19549 _suppressedIframes.set(iframe, iframe.style.pointerEvents);
19550 iframe.style.pointerEvents = "none";
19551 });
19552 }
19553 function restoreIframePointerEvents() {
19554 _suppressedIframes.forEach((prev, iframe) => {
19555 iframe.style.pointerEvents = prev;
19556 });
19557 _suppressedIframes.clear();
19558 }
19559 function findIframeAtCursor(clientX, clientY) {
19560 const win = findWindowRootAtPoint(clientX, clientY);
19561 if (!win) {
19562 return null;
19563 }
19564 const iframe = win.querySelector(IFRAME_SELECTOR);
19565 return iframe instanceof HTMLIFrameElement ? iframe : null;
19566 }
19567 const onBridgeDragOver = (e) => {
19568 if (!_bridgeInterceptPayload) {
19569 return;
19570 }
19571 e.preventDefault();
19572 if (e.dataTransfer) {
19573 e.dataTransfer.dropEffect = "copy";
19574 }
19575 const iframe = findIframeAtCursor(e.clientX, e.clientY);
19576 if (iframe === _lastHoveredBridgeIframe) {
19577 return;
19578 }
19579 if (_lastHoveredBridgeIframe) {
19580 postIntoIframe(_lastHoveredBridgeIframe, {
19581 type: "desktop-mode-drag-leave"
19582 });
19583 }
19584 _lastHoveredBridgeIframe = iframe;
19585 if (iframe) {
19586 postIntoIframe(iframe, {
19587 type: "desktop-mode-drag-over",
19588 payload: _bridgeInterceptPayload
19589 });
19590 }
19591 };
19592 const onBridgeDrop = (e) => {
19593 if (!_bridgeInterceptPayload) {
19594 return;
19595 }
19596 e.preventDefault();
19597 e.stopPropagation();
19598 if (typeof e.stopImmediatePropagation === "function") {
19599 e.stopImmediatePropagation();
19600 }
19601 const iframe = findIframeAtCursor(e.clientX, e.clientY);
19602 const payload = _bridgeInterceptPayload;
19603 stopBridgeIntercept();
19604 if (!iframe) {
19605 return;
19606 }
19607 const rect = iframe.getBoundingClientRect();
19608 postIntoIframe(iframe, {
19609 type: "desktop-mode-drop",
19610 payload,
19611 position: {
19612 x: e.clientX - rect.left,
19613 y: e.clientY - rect.top
19614 }
19615 });
19616 };
19617 const onBridgeDragEnd = () => {
19618 stopBridgeIntercept();
19619 };
19620 function startBridgeIntercept(payload) {
19621 if (_bridgeInterceptPayload) {
19622 _bridgeInterceptPayload = payload;
19623 return;
19624 }
19625 _bridgeInterceptPayload = payload;
19626 suppressIframePointerEventsBridge();
19627 document.addEventListener("dragover", onBridgeDragOver, true);
19628 document.addEventListener("drop", onBridgeDrop, true);
19629 document.addEventListener("dragend", onBridgeDragEnd, true);
19630 }
19631 function stopBridgeIntercept() {
19632 if (!_bridgeInterceptPayload) {
19633 return;
19634 }
19635 _bridgeInterceptPayload = null;
19636 if (_lastHoveredBridgeIframe) {
19637 postIntoIframe(_lastHoveredBridgeIframe, {
19638 type: "desktop-mode-drag-leave"
19639 });
19640 _lastHoveredBridgeIframe = null;
19641 }
19642 document.removeEventListener("dragover", onBridgeDragOver, true);
19643 document.removeEventListener("drop", onBridgeDrop, true);
19644 document.removeEventListener("dragend", onBridgeDragEnd, true);
19645 restoreIframePointerEvents();
19646 }
19647 function extractBridgePayload(payload) {
19648 if (!payload || typeof payload !== "object") {
19649 return void 0;
19650 }
19651 const obj = payload;
19652 if (obj.type !== "shortcut" && obj.type !== "desktop-file") {
19653 return void 0;
19654 }
19655 const data = obj.data;
19656 return data?.bridgePayload;
19657 }
19658 function postIntoIframe(iframe, msg) {
19659 const w = iframe.contentWindow;
19660 if (!w) {
19661 return;
19662 }
19663 try {
19664 w.postMessage(msg, window.location.origin);
19665 } catch {
19666 }
19667 }
19668 function registerDropTargetFor(dragManager, iframe, target2, windowId) {
19669 return dragManager.registerDropTarget({
19670 id: `${TARGET_ID_PREFIX}${windowId}`,
19671 element: target2,
19672 accept: (payload) => !!extractBridgePayload(payload),
19673 onEnter: (session) => {
19674 const bridge = extractBridgePayload(session.payload);
19675 if (!bridge) {
19676 return;
19677 }
19678 target2.setAttribute(DROP_ACTIVE_ATTR$1, "");
19679 postIntoIframe(iframe, {
19680 type: "desktop-mode-drag-over",
19681 payload: bridge
19682 });
19683 },
19684 onLeave: () => {
19685 target2.removeAttribute(DROP_ACTIVE_ATTR$1);
19686 postIntoIframe(iframe, { type: "desktop-mode-drag-leave" });
19687 },
19688 onDrop: (session, ev) => {
19689 target2.removeAttribute(DROP_ACTIVE_ATTR$1);
19690 const bridge = extractBridgePayload(session.payload);
19691 if (!bridge) {
19692 return;
19693 }
19694 const rect = iframe.getBoundingClientRect();
19695 postIntoIframe(iframe, {
19696 type: "desktop-mode-drop",
19697 payload: bridge,
19698 position: {
19699 x: ev.clientX - rect.left,
19700 y: ev.clientY - rect.top
19701 }
19702 });
19703 }
19704 });
19705 }
19706 function deriveWindowIdFromIframe(iframe) {
19707 let cur = iframe.parentElement;
19708 while (cur) {
19709 if (cur.id.startsWith("wp-window-")) {
19710 return cur.id.slice("wp-window-".length);
19711 }
19712 cur = cur.parentElement;
19713 }
19714 return `unknown-${Math.random().toString(36).slice(2, 10)}`;
19715 }
19716 function onDragStart(payload) {
19717 const dragManager = _dragManager;
19718 if (!dragManager) {
19719 return;
19720 }
19721 const iframes = document.querySelectorAll(IFRAME_SELECTOR);
19722 const isBridgeable = !!extractBridgePayload(payload);
19723 console.info(
19724 "[desktop-mode] drag-start: suppressing %d iframe(s); bridgeable=%s",
19725 iframes.length,
19726 isBridgeable,
19727 payload
19728 );
19729 iframes.forEach((iframe) => {
19730 if (!_suppressedIframes.has(iframe)) {
19731 _suppressedIframes.set(iframe, iframe.style.pointerEvents);
19732 iframe.style.pointerEvents = "none";
19733 }
19734 if (!isBridgeable) {
19735 return;
19736 }
19737 if (_activeRegistrations.has(iframe)) {
19738 return;
19739 }
19740 const dropTargetEl = iframe.parentElement;
19741 if (!dropTargetEl) {
19742 return;
19743 }
19744 const windowId = deriveWindowIdFromIframe(iframe);
19745 const deregister = registerDropTargetFor(
19746 dragManager,
19747 iframe,
19748 dropTargetEl,
19749 windowId
19750 );
19751 _activeRegistrations.set(iframe, deregister);
19752 });
19753 }
19754 function onDragEnd$1() {
19755 _suppressedIframes.forEach((prev, iframe) => {
19756 iframe.style.pointerEvents = prev;
19757 });
19758 _suppressedIframes.clear();
19759 _activeRegistrations.forEach((deregister) => {
19760 try {
19761 deregister();
19762 } catch {
19763 }
19764 });
19765 _activeRegistrations.clear();
19766 }
19767 function installIframeDropTargets(dragManager) {
19768 if (_installed$3) {
19769 return;
19770 }
19771 _installed$3 = true;
19772 _dragManager = dragManager;
19773 document.addEventListener(DRAG_EVENTS.START, (e) => {
19774 const detail = e.detail;
19775 onDragStart(detail?.payload);
19776 });
19777 document.addEventListener(DRAG_EVENTS.END, () => {
19778 onDragEnd$1();
19779 });
19780 document.addEventListener(DRAG_BRIDGE_EVENTS.START, (e) => {
19781 const detail = e.detail;
19782 if (!detail?.payload) {
19783 return;
19784 }
19785 startBridgeIntercept(detail.payload);
19786 });
19787 document.addEventListener(DRAG_BRIDGE_EVENTS.END, () => {
19788 stopBridgeIntercept();
19789 });
19790 addAction(
19791 HOOKS.WINDOW_CLOSED,
19792 "desktop-mode/drag/iframe-drop-targets-window-close",
19793 () => {
19794 for (const [iframe] of Array.from(_suppressedIframes)) {
19795 if (!iframe.isConnected) {
19796 _suppressedIframes.delete(iframe);
19797 }
19798 }
19799 for (const [iframe, deregister] of Array.from(_activeRegistrations)) {
19800 if (!iframe.isConnected) {
19801 try {
19802 deregister();
19803 } catch {
19804 }
19805 _activeRegistrations.delete(iframe);
19806 }
19807 }
19808 }
19809 );
19810 window.__desktopModeIframeDropDebug = () => ({
19811 installed: _installed$3,
19812 iframesInDom: document.querySelectorAll(IFRAME_SELECTOR).length,
19813 suppressedCount: _suppressedIframes.size,
19814 registeredCount: _activeRegistrations.size,
19815 suppressedIframeIds: Array.from(_suppressedIframes.keys()).map(
19816 deriveWindowIdFromIframe
19817 )
19818 });
19819 }
19820 const FOCUS_ON_DRAG_HOVER_DWELL_MS = 250;
19821 const FOCUS_ON_DRAG_HOVER_WATCHDOG_MS = 1e3;
19822 const DRAG_HOVER_MESSAGE_TYPE = "desktop-mode-drag-hover";
19823 let _installed$2 = false;
19824 let _host = null;
19825 let _lastHoverWindowId = null;
19826 let _dwellTimer = null;
19827 let _watchdogTimer = null;
19828 let _bridgePayloadKind = null;
19829 function clearDwell() {
19830 if (_dwellTimer !== null) {
19831 clearTimeout(_dwellTimer);
19832 _dwellTimer = null;
19833 }
19834 }
19835 function clearWatchdog() {
19836 if (_watchdogTimer !== null) {
19837 clearTimeout(_watchdogTimer);
19838 _watchdogTimer = null;
19839 }
19840 }
19841 function resetHoverState() {
19842 clearDwell();
19843 clearWatchdog();
19844 _lastHoverWindowId = null;
19845 }
19846 function bumpWatchdog() {
19847 clearWatchdog();
19848 _watchdogTimer = setTimeout(() => {
19849 _watchdogTimer = null;
19850 resetHoverState();
19851 }, FOCUS_ON_DRAG_HOVER_WATCHDOG_MS);
19852 }
19853 function fireFocus(windowId, payloadType) {
19854 const win = _host?.getById(windowId);
19855 if (!win || win.isFocused()) {
19856 return;
19857 }
19858 const shouldFocus = applyFilters(
19859 HOOKS.WINDOW_FOCUS_ON_DRAG_HOVER,
19860 true,
19861 { windowId, payloadType }
19862 );
19863 if (!shouldFocus) {
19864 return;
19865 }
19866 try {
19867 _host?.focus(win);
19868 } catch (err) {
19869 console.error("[desktop-mode] focus-on-drag-hover focus() threw:", windowId, err);
19870 }
19871 }
19872 function trackHoverWindowId(windowId, payloadType) {
19873 if (windowId === _lastHoverWindowId) {
19874 return;
19875 }
19876 clearDwell();
19877 _lastHoverWindowId = windowId;
19878 if (windowId === null) {
19879 return;
19880 }
19881 _dwellTimer = setTimeout(() => {
19882 _dwellTimer = null;
19883 fireFocus(windowId, payloadType);
19884 }, FOCUS_ON_DRAG_HOVER_DWELL_MS);
19885 }
19886 function trackHoverAtPoint(clientX, clientY, payloadType) {
19887 const root = findWindowRootAtPoint(clientX, clientY);
19888 trackHoverWindowId(root ? windowIdFromRoot(root) : null, payloadType);
19889 }
19890 const onDragMove = (e) => {
19891 const detail = e.detail;
19892 if (typeof detail?.clientX !== "number" || typeof detail?.clientY !== "number") {
19893 return;
19894 }
19895 trackHoverAtPoint(detail.clientX, detail.clientY, detail.payload?.type ?? "");
19896 };
19897 const onDragEnd = () => {
19898 resetHoverState();
19899 };
19900 function dragHasFiles$1(e) {
19901 const types = e.dataTransfer?.types;
19902 if (!types) {
19903 return false;
19904 }
19905 const list2 = types;
19906 if (typeof list2.includes === "function") {
19907 return list2.includes("Files");
19908 }
19909 return typeof list2.contains === "function" && list2.contains("Files");
19910 }
19911 const onNativeDragOver = (e) => {
19912 bumpWatchdog();
19913 const payloadType = _bridgePayloadKind ?? (dragHasFiles$1(e) ? "os-file" : "external");
19914 trackHoverAtPoint(e.clientX, e.clientY, payloadType);
19915 };
19916 const onNativeDragSettled = () => {
19917 resetHoverState();
19918 };
19919 const onNativeDragLeave = (e) => {
19920 if (e.relatedTarget === null) {
19921 resetHoverState();
19922 }
19923 };
19924 function windowIdFromMessageSource(source) {
19925 if (!source) {
19926 return null;
19927 }
19928 const iframes = document.querySelectorAll("iframe");
19929 for (const f of Array.from(iframes)) {
19930 if (f.contentWindow === source) {
19931 const host = f.closest("[data-window-id]");
19932 return host?.getAttribute("data-window-id") || null;
19933 }
19934 }
19935 return null;
19936 }
19937 const onHoverMessage = (e) => {
19938 if (e.origin !== window.location.origin) {
19939 return;
19940 }
19941 const data = e.data;
19942 if (!data || data.type !== DRAG_HOVER_MESSAGE_TYPE) {
19943 return;
19944 }
19945 const windowId = windowIdFromMessageSource(e.source);
19946 if (!windowId) {
19947 return;
19948 }
19949 bumpWatchdog();
19950 trackHoverWindowId(
19951 windowId,
19952 typeof data.payloadType === "string" ? data.payloadType : "external"
19953 );
19954 };
19955 const onBridgeStart = (e) => {
19956 const detail = e.detail;
19957 if (detail?.payload) {
19958 _bridgePayloadKind = detail.payload.kind ?? "";
19959 }
19960 };
19961 const onBridgeEnd = () => {
19962 _bridgePayloadKind = null;
19963 resetHoverState();
19964 };
19965 function installFocusWindowOnDragHover(host) {
19966 if (_installed$2) {
19967 return;
19968 }
19969 _installed$2 = true;
19970 _host = host;
19971 document.addEventListener(DRAG_EVENTS.MOVE, onDragMove);
19972 document.addEventListener(DRAG_EVENTS.END, onDragEnd);
19973 document.addEventListener(DRAG_BRIDGE_EVENTS.START, onBridgeStart);
19974 document.addEventListener(DRAG_BRIDGE_EVENTS.END, onBridgeEnd);
19975 document.addEventListener("dragover", onNativeDragOver, true);
19976 document.addEventListener("drop", onNativeDragSettled, true);
19977 document.addEventListener("dragend", onNativeDragSettled, true);
19978 document.addEventListener("dragleave", onNativeDragLeave, true);
19979 window.addEventListener("message", onHoverMessage);
19980 }
19981 function collectOpenables() {
19982 const desktop = window.wp?.desktop;
19983 if (!desktop) {
19984 return [];
19985 }
19986 const wm = desktop.windowManager;
19987 const config = desktop.config;
19988 if (!wm || !config) {
19989 return [];
19990 }
19991 const items = [];
19992 const fromMenu = (item, group) => ({
19993 id: item.id,
19994 label: item.title,
19995 description: group,
19996 icon: item.icon,
19997 open: () => wm.open({
19998 id: item.id,
19999 baseId: item.id,
20000 url: item.url,
20001 title: item.title,
20002 icon: item.icon
20003 })
20004 });
20005 for (const item of config.dockItems ?? []) {
20006 items.push(fromMenu(item, "Admin menu"));
20007 }
20008 const filtered = applyFilters(
20009 "desktop-mode.open-command.items",
20010 items
20011 );
20012 return Array.isArray(filtered) ? filtered : items;
20013 }
20014 const openCommand = {
20015 slug: "open",
20016 label: "Open",
20017 description: "Open an admin page or registered window.",
20018 hint: "[window]",
20019 icon: "dashicons-external",
20020 /**
20021 * Suggest matching windows as the user types args. Simple
20022 * case-insensitive substring match against label AND id so
20023 * "add" finds "Add New Post" and "jorvy" finds Jorvy whether
20024 * the plugin listed it with a friendly label or the slug.
20025 */
20026 suggest(args) {
20027 const q = args.trim().toLowerCase();
20028 const list2 = collectOpenables();
20029 const hits = q === "" ? list2 : list2.filter(
20030 (w) => w.label.toLowerCase().includes(q) || w.id.toLowerCase().includes(q)
20031 );
20032 return hits.slice(0, 12).map((w) => ({
20033 value: w.label,
20034 label: w.label,
20035 description: w.description,
20036 icon: w.icon ?? "dashicons-external"
20037 }));
20038 },
20039 run(args, ctx) {
20040 const q = args.trim();
20041 if (!q) {
20042 return "Type the name of a window to open, for example `/open Posts`.";
20043 }
20044 const list2 = collectOpenables();
20045 const ql = q.toLowerCase();
20046 const match = list2.find((w) => w.label.toLowerCase() === ql || w.id.toLowerCase() === ql) ?? list2.find(
20047 (w) => w.label.toLowerCase().includes(ql) || w.id.toLowerCase().includes(ql)
20048 );
20049 if (!match) {
20050 return `No window matching **${q}** — try \`/open\` alone to see available options.`;
20051 }
20052 match.open();
20053 ctx.close();
20054 }
20055 };
20056 function registerBuiltInCommands() {
20057 registerCommand(openCommand);
20058 }
20059 const palettes = [];
20060 const listeners$2 = /* @__PURE__ */ new Set();
20061 function registerPalette(p) {
20062 if (!p || typeof p.id !== "string" || p.id === "") {
20063 return () => {
20064 };
20065 }
20066 if (typeof p.open !== "function" || typeof p.close !== "function" || typeof p.isOpen !== "function") {
20067 return () => {
20068 };
20069 }
20070 const idx = palettes.findIndex((x) => x.id === p.id);
20071 if (idx >= 0) {
20072 palettes[idx] = p;
20073 } else {
20074 palettes.push(p);
20075 }
20076 notify$2();
20077 return () => {
20078 const i = palettes.findIndex((x) => x.id === p.id);
20079 if (i >= 0) {
20080 palettes.splice(i, 1);
20081 notify$2();
20082 }
20083 };
20084 }
20085 function unregisterPalette(id) {
20086 const idx = palettes.findIndex((x) => x.id === id);
20087 if (idx >= 0) {
20088 palettes.splice(idx, 1);
20089 notify$2();
20090 }
20091 }
20092 function listPalettes() {
20093 return palettes.slice();
20094 }
20095 function notify$2() {
20096 for (const cb of Array.from(listeners$2)) {
20097 try {
20098 cb();
20099 } catch (err) {
20100 if (typeof console !== "undefined") {
20101 console.error("[desktop-mode] palette-registry listener threw:", err);
20102 }
20103 }
20104 }
20105 }
20106 function cyclePalettes() {
20107 if (palettes.length === 0) {
20108 return;
20109 }
20110 const cur = palettes.findIndex((p) => {
20111 try {
20112 return p.isOpen();
20113 } catch {
20114 return false;
20115 }
20116 });
20117 if (cur === -1) {
20118 try {
20119 palettes[0].open();
20120 } catch {
20121 }
20122 return;
20123 }
20124 try {
20125 palettes[cur].close();
20126 } catch {
20127 }
20128 const next = cur + 1;
20129 if (next < palettes.length) {
20130 try {
20131 palettes[next].open();
20132 } catch {
20133 }
20134 }
20135 }
20136 function openPaletteOnly(id) {
20137 const target2 = palettes.find((p) => p.id === id);
20138 if (!target2) {
20139 return;
20140 }
20141 for (const p of palettes) {
20142 if (p.id !== id) {
20143 try {
20144 if (p.isOpen()) {
20145 p.close();
20146 }
20147 } catch {
20148 }
20149 }
20150 }
20151 try {
20152 target2.open();
20153 } catch {
20154 }
20155 }
20156 let installed$1 = false;
20157 function installPaletteShortcut() {
20158 if (installed$1) {
20159 return;
20160 }
20161 installed$1 = true;
20162 document.addEventListener(
20163 "keydown",
20164 (e) => {
20165 if (!(e.metaKey || e.ctrlKey) || e.key !== "k") {
20166 return;
20167 }
20168 if (e.shiftKey || e.altKey) {
20169 return;
20170 }
20171 e.preventDefault();
20172 e.stopImmediatePropagation();
20173 cyclePalettes();
20174 },
20175 true
20176 );
20177 const origin = window.location.origin;
20178 window.addEventListener("message", (e) => {
20179 if (e.origin !== origin) {
20180 return;
20181 }
20182 const data = e.data;
20183 if (data && data.type === "desktop-mode-palette-cycle") {
20184 cyclePalettes();
20185 }
20186 });
20187 }
20188 const store$2 = createSharedStore(
20189 "desktop-mode/presence",
20190 () => ({ byUser: /* @__PURE__ */ new Map(), serverTimeMs: 0 })
20191 );
20192 const ACTIVE_THRESHOLD_MS = 5 * 60 * 1e3;
20193 let lastInputMs = Date.now();
20194 let booted$2 = false;
20195 function noteUserActivity() {
20196 lastInputMs = Date.now();
20197 }
20198 function applySnapshot(block) {
20199 if (!block || !block.snapshot) {
20200 return;
20201 }
20202 const previous = store$2.state.byUser;
20203 const next = new Map(previous);
20204 const transitions = [];
20205 for (const [rawId, raw] of Object.entries(block.snapshot)) {
20206 const userId = Number(rawId);
20207 if (!Number.isFinite(userId) || userId <= 0) {
20208 continue;
20209 }
20210 const status = raw?.status ?? "offline";
20211 const entry = {
20212 status,
20213 lastSeenMs: Number(raw?.lastSeenMs ?? 0) || 0,
20214 lastActiveMs: Number(raw?.lastActiveMs ?? 0) || 0
20215 };
20216 const old = previous.get(userId);
20217 next.set(userId, entry);
20218 if (!old || old.status !== entry.status) {
20219 transitions.push({
20220 userId,
20221 oldStatus: old ? old.status : null,
20222 newStatus: entry.status,
20223 entry
20224 });
20225 }
20226 }
20227 store$2.state.byUser = next;
20228 if (typeof block.serverTimeMs === "number") {
20229 store$2.state.serverTimeMs = block.serverTimeMs;
20230 }
20231 store$2.notify();
20232 for (const t of transitions) {
20233 const detail = {
20234 userId: t.userId,
20235 oldStatus: t.oldStatus,
20236 newStatus: t.newStatus,
20237 lastSeenMs: t.entry.lastSeenMs,
20238 lastActiveMs: t.entry.lastActiveMs
20239 };
20240 document.dispatchEvent(
20241 new CustomEvent("desktop-mode-presence-changed", { detail })
20242 );
20243 activity.publish("desktop-mode/presence-changed", detail);
20244 }
20245 activity.publish("desktop-mode/presence-snapshot-applied", {
20246 applied: Object.keys(block.snapshot).length,
20247 transitions: transitions.length
20248 });
20249 }
20250 function bootPresenceProbe() {
20251 if (booted$2) {
20252 return;
20253 }
20254 booted$2 = true;
20255 document.addEventListener("pointerdown", noteUserActivity, {
20256 capture: true,
20257 passive: true
20258 });
20259 document.addEventListener("keydown", noteUserActivity, {
20260 capture: true,
20261 passive: true
20262 });
20263 document.addEventListener("visibilitychange", () => {
20264 if (!document.hidden) {
20265 noteUserActivity();
20266 }
20267 });
20268 heartbeat.contribute("desktop_mode_presence_active", () => true);
20269 heartbeat.contribute(
20270 "desktop_mode_user_active",
20271 () => Date.now() - lastInputMs < ACTIVE_THRESHOLD_MS
20272 );
20273 heartbeat.subscribe("desktop_mode_presence", (block) => {
20274 applySnapshot(block);
20275 });
20276 }
20277 function getStatus(userId) {
20278 const entry = store$2.state.byUser.get(userId);
20279 return entry ? entry.status : "offline";
20280 }
20281 function getAll() {
20282 return new Map(store$2.state.byUser);
20283 }
20284 function getEntry(userId) {
20285 return store$2.state.byUser.get(userId) ?? null;
20286 }
20287 function subscribe$1(cb) {
20288 return store$2.subscribe((s) => cb(s));
20289 }
20290 function markActive() {
20291 noteUserActivity();
20292 }
20293 function applyPresenceBatch(updates) {
20294 if (!Array.isArray(updates) || updates.length === 0) {
20295 return;
20296 }
20297 const previous = store$2.state.byUser;
20298 const next = new Map(previous);
20299 const transitions = [];
20300 for (const u of updates) {
20301 const userId = Number(u.userId);
20302 if (!Number.isFinite(userId) || userId <= 0) {
20303 continue;
20304 }
20305 const old = previous.get(userId);
20306 const entry = {
20307 status: u.status,
20308 lastSeenMs: typeof u.lastSeenMs === "number" ? u.lastSeenMs : old?.lastSeenMs ?? 0,
20309 lastActiveMs: typeof u.lastActiveMs === "number" ? u.lastActiveMs : old?.lastActiveMs ?? 0
20310 };
20311 next.set(userId, entry);
20312 if (!old || old.status !== entry.status) {
20313 transitions.push({
20314 userId,
20315 oldStatus: old ? old.status : null,
20316 newStatus: entry.status,
20317 entry
20318 });
20319 }
20320 }
20321 if (transitions.length === 0 && next.size === previous.size) {
20322 return;
20323 }
20324 store$2.state.byUser = next;
20325 store$2.notify();
20326 for (const t of transitions) {
20327 const detail = {
20328 userId: t.userId,
20329 oldStatus: t.oldStatus,
20330 newStatus: t.newStatus,
20331 lastSeenMs: t.entry.lastSeenMs,
20332 lastActiveMs: t.entry.lastActiveMs
20333 };
20334 document.dispatchEvent(
20335 new CustomEvent("desktop-mode-presence-changed", { detail })
20336 );
20337 activity.publish("desktop-mode/presence-changed", detail);
20338 }
20339 activity.publish("desktop-mode/presence-snapshot-applied", {
20340 applied: updates.length,
20341 transitions: transitions.length
20342 });
20343 }
20344 const presenceApi = Object.freeze({
20345 getStatus,
20346 getAll,
20347 getEntry,
20348 subscribe: subscribe$1,
20349 markActive,
20350 applyBatch: applyPresenceBatch
20351 });
20352 let seenTs = null;
20353 function bootContentChangesHeartbeat() {
20354 heartbeat.contribute(
20355 "desktop_mode_content_changes_seen_ts",
20356 () => seenTs === null ? 0 : seenTs
20357 );
20358 heartbeat.subscribe(
20359 "desktop_mode_content_changes",
20360 (block) => {
20361 if (!block || typeof block.ts !== "number") {
20362 return;
20363 }
20364 const handshake = seenTs === null;
20365 const floor = seenTs ?? 0;
20366 let maxTs = Math.max(floor, block.ts);
20367 if (!handshake && Array.isArray(block.entries)) {
20368 for (const entry of block.entries) {
20369 if (!entry || typeof entry.ts !== "number" || entry.ts <= floor || typeof entry.type !== "string" || entry.type === "") {
20370 continue;
20371 }
20372 broadcast(`desktop-mode.${entry.type}.changed`, {
20373 source: "heartbeat",
20374 action: typeof entry.action === "string" && entry.action !== "" ? entry.action : "updated",
20375 ids: Array.isArray(entry.ids) ? entry.ids.map(Number).filter((id) => id > 0) : []
20376 });
20377 if (entry.ts > maxTs) {
20378 maxTs = entry.ts;
20379 }
20380 }
20381 }
20382 seenTs = maxTs;
20383 }
20384 );
20385 }
20386 const HEARTBEAT_FIELD = "desktop_mode_nonces";
20387 const targets = /* @__PURE__ */ new Map();
20388 let booted$1 = false;
20389 function registerNonceTarget(action, updater) {
20390 if (typeof action !== "string" || action === "") {
20391 return () => {
20392 };
20393 }
20394 let set = targets.get(action);
20395 if (!set) {
20396 set = /* @__PURE__ */ new Set();
20397 targets.set(action, set);
20398 }
20399 set.add(updater);
20400 return () => {
20401 set.delete(updater);
20402 };
20403 }
20404 function bootNonceRefresh() {
20405 if (booted$1) {
20406 return;
20407 }
20408 booted$1 = true;
20409 heartbeat.subscribe(HEARTBEAT_FIELD, (payload) => {
20410 if (!payload || typeof payload !== "object") {
20411 return;
20412 }
20413 for (const [action, value] of Object.entries(payload)) {
20414 if (typeof value !== "string" || value === "") {
20415 continue;
20416 }
20417 const set = targets.get(action);
20418 if (!set) {
20419 continue;
20420 }
20421 for (const updater of set) {
20422 try {
20423 updater(value);
20424 } catch (err) {
20425 console.error(
20426 `[desktop-mode/nonce-refresh] updater for "${action}" threw:`,
20427 err
20428 );
20429 }
20430 }
20431 }
20432 });
20433 registerShellAndPluginsWindowTargets();
20434 }
20435 function registerShellAndPluginsWindowTargets() {
20436 registerNonceTarget("wp_rest", updateAllRestNonces);
20437 registerNonceTarget("desktop-mode-plugins", (fresh) => {
20438 writeWindowConfigField("desktop-mode-plugins", "ajaxNonce", fresh);
20439 });
20440 registerNonceTarget("updates", (fresh) => {
20441 writeWindowConfigField("desktop-mode-plugins", "updatesNonce", fresh);
20442 });
20443 }
20444 function updateAllRestNonces(fresh) {
20445 const cfg = readShellConfig();
20446 if (cfg && typeof cfg.restNonce === "string") {
20447 cfg.restNonce = fresh;
20448 }
20449 const windowConfigs = readWindowConfigs();
20450 if (!windowConfigs) {
20451 return;
20452 }
20453 for (const blob of Object.values(windowConfigs)) {
20454 if (blob && typeof blob === "object" && typeof blob.restNonce === "string") {
20455 blob.restNonce = fresh;
20456 }
20457 }
20458 }
20459 function writeWindowConfigField(windowId, field, value) {
20460 const blobs = readWindowConfigs();
20461 const blob = blobs?.[windowId];
20462 if (blob && typeof blob === "object") {
20463 blob[field] = value;
20464 }
20465 }
20466 function readShellConfig() {
20467 if (typeof window === "undefined") {
20468 return void 0;
20469 }
20470 return window.desktopModeConfig;
20471 }
20472 function readWindowConfigs() {
20473 if (typeof window === "undefined") {
20474 return void 0;
20475 }
20476 return window.desktopModeWindowConfig;
20477 }
20478 const AUTH_FIELD = "desktop_mode_auth";
20479 const FAILURE_COOLDOWN_MS = 5e3;
20480 const TICK_COOLDOWN_MS = 1e3;
20481 const RECOVERY_COOLDOWN_MS = 1e4;
20482 let booted = false;
20483 let sawLoggedOut = false;
20484 let authLostAnnounced = false;
20485 let bootUid = 0;
20486 let failureCooldownUntil = 0;
20487 let tickCooldownUntil = 0;
20488 let tickTimer = null;
20489 let lastRecoveryAt = 0;
20490 let messageListener = null;
20491 let modalObserver = null;
20492 let reloadShell = () => {
20493 try {
20494 window.location.reload();
20495 } catch {
20496 }
20497 };
20498 function connectNow() {
20499 try {
20500 const hb = window.wp?.heartbeat;
20501 if (hb && typeof hb.connectNow === "function") {
20502 hb.connectNow();
20503 }
20504 } catch {
20505 }
20506 }
20507 function forceTickSoon(cooldownMs = TICK_COOLDOWN_MS) {
20508 const now = Date.now();
20509 if (now < tickCooldownUntil) {
20510 if (tickTimer === null) {
20511 tickTimer = window.setTimeout(() => {
20512 tickTimer = null;
20513 tickCooldownUntil = Date.now() + TICK_COOLDOWN_MS;
20514 connectNow();
20515 }, tickCooldownUntil - now);
20516 }
20517 return;
20518 }
20519 tickCooldownUntil = now + cooldownMs;
20520 connectNow();
20521 }
20522 function announceAuthLost() {
20523 sawLoggedOut = true;
20524 if (authLostAnnounced) {
20525 return;
20526 }
20527 authLostAnnounced = true;
20528 doAction(HOOKS.AUTH_LOST);
20529 document.dispatchEvent(new CustomEvent("desktop-mode-auth-lost"));
20530 }
20531 function reloadChromelessIframes() {
20532 for (const frame of _reloadableIframes()) {
20533 try {
20534 frame.contentWindow?.location.reload();
20535 } catch {
20536 }
20537 }
20538 }
20539 function _reloadableIframes() {
20540 let frames;
20541 try {
20542 frames = document.querySelectorAll("iframe");
20543 } catch {
20544 return [];
20545 }
20546 return Array.from(frames).filter(
20547 (frame) => frame.id !== "wp-auth-check-frame" && !frame.closest("#wp-auth-check-wrap")
20548 );
20549 }
20550 function runRecovery() {
20551 const now = Date.now();
20552 if (now - lastRecoveryAt < RECOVERY_COOLDOWN_MS) {
20553 return;
20554 }
20555 lastRecoveryAt = now;
20556 sawLoggedOut = false;
20557 authLostAnnounced = false;
20558 if (tickTimer !== null) {
20559 window.clearTimeout(tickTimer);
20560 tickTimer = null;
20561 }
20562 tickCooldownUntil = 0;
20563 forceTickSoon();
20564 reloadChromelessIframes();
20565 doAction(HOOKS.AUTH_RESTORED);
20566 document.dispatchEvent(new CustomEvent("desktop-mode-auth-restored"));
20567 }
20568 function checkUid(value) {
20569 const uid = value && typeof value === "object" ? Number(value.uid) : NaN;
20570 if (!Number.isFinite(uid) || uid <= 0) {
20571 return;
20572 }
20573 if (bootUid <= 0) {
20574 bootUid = uid;
20575 return;
20576 }
20577 if (uid !== bootUid) {
20578 reloadShell();
20579 }
20580 }
20581 function noteAuthFailure(status, url) {
20582 if (status !== 401 && status !== 403) {
20583 return;
20584 }
20585 let resolved;
20586 try {
20587 resolved = new URL(String(url || ""), window.location.href);
20588 } catch {
20589 return;
20590 }
20591 if (resolved.origin !== window.location.origin) {
20592 return;
20593 }
20594 if (resolved.pathname.indexOf("/wp-admin/admin-ajax.php") !== -1 && /(?:^|&|\?)action=heartbeat(?:&|$)/.test(resolved.search)) {
20595 return;
20596 }
20597 if (resolved.pathname.indexOf("/wp-login.php") !== -1) {
20598 return;
20599 }
20600 const now = Date.now();
20601 if (now < failureCooldownUntil) {
20602 return;
20603 }
20604 failureCooldownUntil = now + FAILURE_COOLDOWN_MS;
20605 connectNow();
20606 }
20607 function observeAuthCheckModal() {
20608 const wrap = document.getElementById("wp-auth-check-wrap");
20609 if (!wrap || typeof MutationObserver === "undefined") {
20610 return;
20611 }
20612 let wasVisible = !wrap.classList.contains("hidden");
20613 modalObserver = new MutationObserver(() => {
20614 const visible = !wrap.classList.contains("hidden");
20615 if (wasVisible && !visible) {
20616 forceTickSoon();
20617 }
20618 wasVisible = visible;
20619 });
20620 modalObserver.observe(wrap, {
20621 attributes: true,
20622 attributeFilter: ["class"]
20623 });
20624 }
20625 function bootAuthRecovery(opts = {}) {
20626 if (booted) {
20627 return;
20628 }
20629 booted = true;
20630 bootUid = Number(opts.currentUserId) > 0 ? Number(opts.currentUserId) : 0;
20631 if (opts.reloadShell) {
20632 reloadShell = opts.reloadShell;
20633 }
20634 heartbeat.subscribe("wp-auth-check", (value) => {
20635 if (value === false) {
20636 announceAuthLost();
20637 return;
20638 }
20639 if (value === true && sawLoggedOut) {
20640 runRecovery();
20641 }
20642 });
20643 heartbeat.subscribe("nonces_expired", () => {
20644 if (sawLoggedOut) {
20645 runRecovery();
20646 return;
20647 }
20648 forceTickSoon();
20649 });
20650 heartbeat.subscribe(AUTH_FIELD, checkUid);
20651 messageListener = (ev) => {
20652 if (ev.origin !== window.location.origin) {
20653 return;
20654 }
20655 const data = ev.data;
20656 if (!data || typeof data !== "object") {
20657 return;
20658 }
20659 if (data.type === "desktop-mode-reauth-detected") {
20660 runRecovery();
20661 }
20662 };
20663 window.addEventListener("message", messageListener);
20664 observeAuthCheckModal();
20665 }
20666 const VIEWPORT_CLAMP_MARGIN = 12;
20667 function findDockEntryForUrl(url, config) {
20668 const windowId = deriveWindowId(url, config.adminUrl);
20669 return (config.dockItems || []).find(
20670 (i) => deriveWindowId(i.url, config.adminUrl) === windowId || (i.submenu || []).some(
20671 (s) => deriveWindowId(s.url, config.adminUrl) === windowId
20672 )
20673 );
20674 }
20675 function clampGeometryToViewport(win, rect) {
20676 const maxW = Math.max(200, rect.width - VIEWPORT_CLAMP_MARGIN * 2);
20677 const maxH = Math.max(200, rect.height - VIEWPORT_CLAMP_MARGIN * 2);
20678 const width = Math.min(win.width, maxW);
20679 const height = Math.min(win.height, maxH);
20680 const maxX = Math.max(0, rect.width - width - VIEWPORT_CLAMP_MARGIN);
20681 const maxY = Math.max(0, rect.height - height - VIEWPORT_CLAMP_MARGIN);
20682 const x = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.x, maxX));
20683 const y = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.y, maxY));
20684 return { x, y, width, height };
20685 }
20686 const INITIAL_ORIGIN$1 = window.location.origin;
20687 function bindTopWindowLinkInterceptor(manager2, config) {
20688 document.addEventListener(
20689 "click",
20690 (e) => {
20691 if (e.defaultPrevented) {
20692 return;
20693 }
20694 if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
20695 return;
20696 }
20697 const target2 = e.target;
20698 const link = target2 && target2.closest ? target2.closest("a[href]") : null;
20699 if (!link) {
20700 return;
20701 }
20702 const anchor = link;
20703 const linkTarget = anchor.getAttribute("target");
20704 if (linkTarget && linkTarget !== "" && linkTarget !== "_self") {
20705 return;
20706 }
20707 if (anchor.hasAttribute("download")) {
20708 return;
20709 }
20710 const rawHref = anchor.getAttribute("href");
20711 if (!rawHref || rawHref.charAt(0) === "#") {
20712 return;
20713 }
20714 if (/^(mailto:|tel:|javascript:|data:)/i.test(rawHref)) {
20715 return;
20716 }
20717 let url;
20718 try {
20719 url = new URL(rawHref, window.location.href);
20720 } catch (err) {
20721 if (typeof console !== "undefined") {
20722 console.warn(
20723 "[desktop-mode] Couldn’t parse href; letting the browser handle the click:",
20724 rawHref,
20725 err
20726 );
20727 }
20728 return;
20729 }
20730 if (url.origin !== INITIAL_ORIGIN$1) {
20731 return;
20732 }
20733 let adminPath;
20734 try {
20735 adminPath = new URL(config.adminUrl).pathname;
20736 } catch (err) {
20737 if (typeof console !== "undefined") {
20738 console.error(
20739 "[desktop-mode] config.adminUrl is not a valid URL; falling back to /wp-admin/:",
20740 config.adminUrl,
20741 err
20742 );
20743 }
20744 adminPath = "/wp-admin/";
20745 }
20746 if (!url.pathname.startsWith(adminPath)) {
20747 return;
20748 }
20749 if (/\/(admin-post|admin-ajax)\.php$/.test(url.pathname)) {
20750 return;
20751 }
20752 if (url.searchParams.has("action") && url.searchParams.get("action") === "logout") {
20753 return;
20754 }
20755 if (url.searchParams.has("desktop_mode_classic")) {
20756 return;
20757 }
20758 e.preventDefault();
20759 e.stopPropagation();
20760 if (tryNativeUrlRemap(url.href)) {
20761 return;
20762 }
20763 const windowId = deriveWindowId(url.href, config.adminUrl);
20764 const dockEntry = findDockEntryForUrl(url.href, config);
20765 const fallbackTitle = (anchor.textContent || "").trim() || dockEntry?.title || "";
20766 const isAdminBarNew = !!anchor.closest("#wp-admin-bar-new-content");
20767 const openOpts = {
20768 id: windowId,
20769 baseId: windowId,
20770 multi: !!dockEntry?.multi || isAdminBarNew,
20771 url: url.href,
20772 parentUrl: dockEntry?.url ?? url.href,
20773 title: dockEntry?.title || fallbackTitle,
20774 icon: dockEntry?.icon || "dashicons-admin-generic",
20775 submenu: dockEntry?.submenu
20776 };
20777 if (isAdminBarNew) {
20778 void manager2.openNew(openOpts);
20779 return;
20780 }
20781 void manager2.open(openOpts);
20782 },
20783 true
20784 );
20785 }
20786 const REGISTRY_CHANGED_EVENT = "desktop-mode-registry-changed";
20787 function diffIds(prev, next) {
20788 const prevIds = /* @__PURE__ */ new Set();
20789 if (Array.isArray(prev)) {
20790 for (const item of prev) {
20791 if (item && typeof item.id === "string") {
20792 prevIds.add(item.id);
20793 }
20794 }
20795 }
20796 const nextIds = /* @__PURE__ */ new Set();
20797 for (const item of next) {
20798 if (item && typeof item.id === "string") {
20799 nextIds.add(item.id);
20800 }
20801 }
20802 const added = [];
20803 for (const id of nextIds) {
20804 if (!prevIds.has(id)) {
20805 added.push(id);
20806 }
20807 }
20808 const removed = [];
20809 for (const id of prevIds) {
20810 if (!nextIds.has(id)) {
20811 removed.push(id);
20812 }
20813 }
20814 return { added, removed };
20815 }
20816 function emitRegistryChanged(registry2, prev, next) {
20817 const { added, removed } = diffIds(prev, next);
20818 if (added.length === 0 && removed.length === 0) {
20819 return;
20820 }
20821 if (typeof document === "undefined") {
20822 return;
20823 }
20824 const detail = { registry: registry2, added, removed };
20825 document.dispatchEvent(
20826 new CustomEvent(REGISTRY_CHANGED_EVENT, { detail })
20827 );
20828 }
20829 function createApplyPayload(deps2) {
20830 const {
20831 applyDockItems,
20832 config,
20833 syncNativeWindows,
20834 syncServerWidgets,
20835 syncServerWallpapers,
20836 syncServerCommands,
20837 syncServerSettingsTabs,
20838 syncServerTitleBarButtons,
20839 syncServerUnfocusEffects,
20840 syncServerWindowLinkRenderers,
20841 syncServerDockRailRenderers,
20842 syncServerGames,
20843 renderIcons,
20844 syncShortcuts
20845 } = deps2;
20846 return function applyPayload(payload) {
20847 const dockItems = payload.dockItems;
20848 const nativeWindows = payload.nativeWindows;
20849 const serverWidgets = payload.serverWidgets;
20850 const serverWallpapers = payload.serverWallpapers;
20851 const serverCommandScripts = payload.serverCommandScripts;
20852 const serverCommands = payload.serverCommands;
20853 const serverSettingsTabScripts = payload.serverSettingsTabScripts;
20854 const serverSettingsTabs = payload.serverSettingsTabs;
20855 const serverDockRailRendererScripts = payload.serverDockRailRendererScripts;
20856 const serverTitleBarButtonScripts = payload.serverTitleBarButtonScripts;
20857 const serverUnfocusEffectScripts = payload.serverUnfocusEffectScripts;
20858 const serverWindowLinkRendererScripts = payload.serverWindowLinkRendererScripts;
20859 const serverWindowNotices = payload.serverWindowNotices;
20860 const serverGames = payload.serverGames;
20861 const desktopIcons = payload.desktopIcons;
20862 if (!Array.isArray(dockItems) || dockItems.length === 0) {
20863 return;
20864 }
20865 const prevDockItems = config.dockItems;
20866 applyDockItems(dockItems);
20867 config.dockItems = dockItems;
20868 emitRegistryChanged(
20869 "dock-items",
20870 prevDockItems,
20871 dockItems
20872 );
20873 syncShortcuts?.();
20874 if (Array.isArray(nativeWindows)) {
20875 const prevNativeWindows = config.nativeWindows;
20876 void syncNativeWindows(
20877 nativeWindows
20878 );
20879 config.nativeWindows = nativeWindows;
20880 emitRegistryChanged(
20881 "native-windows",
20882 prevNativeWindows,
20883 nativeWindows
20884 );
20885 }
20886 if (Array.isArray(serverWidgets)) {
20887 void syncServerWidgets(
20888 serverWidgets
20889 );
20890 config.serverWidgets = serverWidgets;
20891 }
20892 if (Array.isArray(serverWallpapers)) {
20893 void syncServerWallpapers(
20894 serverWallpapers
20895 );
20896 config.serverWallpapers = serverWallpapers;
20897 }
20898 if (Array.isArray(serverGames)) {
20899 void syncServerGames(serverGames);
20900 config.serverGames = serverGames;
20901 }
20902 if (Array.isArray(serverCommandScripts)) {
20903 void syncServerCommands(
20904 serverCommandScripts,
20905 Array.isArray(serverCommands) ? serverCommands : void 0
20906 );
20907 config.serverCommandScripts = serverCommandScripts;
20908 if (Array.isArray(serverCommands)) {
20909 config.serverCommands = serverCommands;
20910 }
20911 }
20912 if (Array.isArray(serverSettingsTabScripts)) {
20913 void syncServerSettingsTabs(
20914 serverSettingsTabScripts,
20915 Array.isArray(serverSettingsTabs) ? serverSettingsTabs : void 0
20916 );
20917 config.serverSettingsTabScripts = serverSettingsTabScripts;
20918 if (Array.isArray(serverSettingsTabs)) {
20919 config.serverSettingsTabs = serverSettingsTabs;
20920 }
20921 }
20922 if (Array.isArray(serverTitleBarButtonScripts)) {
20923 void syncServerTitleBarButtons(
20924 serverTitleBarButtonScripts
20925 );
20926 config.serverTitleBarButtonScripts = serverTitleBarButtonScripts;
20927 }
20928 if (Array.isArray(serverUnfocusEffectScripts)) {
20929 void syncServerUnfocusEffects(
20930 serverUnfocusEffectScripts
20931 );
20932 config.serverUnfocusEffectScripts = serverUnfocusEffectScripts;
20933 }
20934 if (Array.isArray(serverWindowLinkRendererScripts)) {
20935 void syncServerWindowLinkRenderers(
20936 serverWindowLinkRendererScripts
20937 );
20938 config.serverWindowLinkRendererScripts = serverWindowLinkRendererScripts;
20939 }
20940 if (Array.isArray(serverDockRailRendererScripts)) {
20941 void syncServerDockRailRenderers(
20942 serverDockRailRendererScripts
20943 );
20944 config.serverDockRailRendererScripts = serverDockRailRendererScripts;
20945 }
20946 if (Array.isArray(serverWindowNotices)) {
20947 applyServerWindowNotices(
20948 serverWindowNotices
20949 );
20950 config.serverWindowNotices = serverWindowNotices;
20951 }
20952 if (Array.isArray(desktopIcons)) {
20953 const prevDesktopIcons = config.desktopIcons;
20954 renderIcons(desktopIcons);
20955 config.desktopIcons = desktopIcons;
20956 emitRegistryChanged(
20957 "desktop-icons",
20958 prevDesktopIcons,
20959 desktopIcons
20960 );
20961 }
20962 };
20963 }
20964 const MENU_REFRESH_TIMEOUT_MS = 8e3;
20965 function bindMenuRefresh(deps2) {
20966 const {
20967 layoutDispatcher,
20968 desktopArea,
20969 config,
20970 syncNativeWindows,
20971 syncServerWidgets,
20972 syncServerWallpapers,
20973 syncServerCommands,
20974 syncServerSettingsTabs,
20975 syncServerTitleBarButtons,
20976 syncServerUnfocusEffects,
20977 syncServerWindowLinkRenderers,
20978 syncServerDockRailRenderers,
20979 syncServerGames,
20980 renderIcons,
20981 syncShortcuts
20982 } = deps2;
20983 const applyPayload = createApplyPayload({
20984 applyDockItems: (items) => layoutDispatcher?.applyDockItems(items),
20985 config,
20986 syncNativeWindows,
20987 syncServerWidgets,
20988 syncServerWallpapers,
20989 syncServerCommands,
20990 syncServerSettingsTabs,
20991 syncServerTitleBarButtons,
20992 syncServerUnfocusEffects,
20993 syncServerWindowLinkRenderers,
20994 syncServerDockRailRenderers,
20995 syncServerGames,
20996 renderIcons,
20997 syncShortcuts
20998 });
20999 let lastMenuSig = typeof config.menuSig === "string" ? config.menuSig : "";
21000 let sigRefreshInFlight = false;
21001 const refresh = () => {
21002 if (!config.adminUrl) {
21003 return Promise.resolve();
21004 }
21005 const probeUrl = (() => {
21006 try {
21007 const url = new URL("admin.php", config.adminUrl);
21008 url.searchParams.set("desktop_mode_chromeless", "1");
21009 url.searchParams.set("desktop_mode_menu_refresh", "1");
21010 return url.toString();
21011 } catch (_err) {
21012 return null;
21013 }
21014 })();
21015 if (!probeUrl) {
21016 return Promise.resolve();
21017 }
21018 return new Promise((resolve2) => {
21019 const iframe = document.createElement("iframe");
21020 iframe.setAttribute("aria-hidden", "true");
21021 iframe.tabIndex = -1;
21022 iframe.style.cssText = "position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;border:0;opacity:0;pointer-events:none;";
21023 iframe.src = probeUrl;
21024 let done = false;
21025 const cleanup = () => {
21026 if (done) {
21027 return;
21028 }
21029 done = true;
21030 window.clearTimeout(timeoutId);
21031 window.removeEventListener("message", onMessage);
21032 if (iframe.parentNode) {
21033 iframe.parentNode.removeChild(iframe);
21034 }
21035 resolve2();
21036 };
21037 const onMessage = (e) => {
21038 if (e.source !== iframe.contentWindow) {
21039 return;
21040 }
21041 const data = e.data;
21042 if (!data || data.type !== "desktop-mode-plugins-changed") {
21043 return;
21044 }
21045 cleanup();
21046 };
21047 const timeoutId = window.setTimeout(() => {
21048 doAction(HOOKS.SHELL_ERROR, {
21049 scope: "menu-refresh",
21050 error: new Error("menu refresh probe timed out")
21051 });
21052 cleanup();
21053 }, MENU_REFRESH_TIMEOUT_MS);
21054 window.addEventListener("message", onMessage);
21055 document.body.appendChild(iframe);
21056 });
21057 };
21058 window.addEventListener("message", (e) => {
21059 if (e.origin !== INITIAL_ORIGIN$1) {
21060 return;
21061 }
21062 const data = e.data;
21063 if (!data) {
21064 return;
21065 }
21066 if (data.type === "desktop-mode-plugins-changed") {
21067 if (data.payload) {
21068 applyPayload(data.payload);
21069 if (typeof data.payload.menuSig === "string") {
21070 lastMenuSig = data.payload.menuSig;
21071 }
21072 }
21073 return;
21074 }
21075 if (data.type === "desktop-mode-menu-signature") {
21076 const sig = data.sig;
21077 if (typeof sig === "string" && sig !== "" && sig !== lastMenuSig && !sigRefreshInFlight) {
21078 sigRefreshInFlight = true;
21079 void refresh().finally(() => {
21080 sigRefreshInFlight = false;
21081 });
21082 }
21083 }
21084 });
21085 return refresh;
21086 }
21087 function hasRestorableSession(session) {
21088 if (!session) {
21089 return false;
21090 }
21091 if (Array.isArray(session.windows) && session.windows.length > 0) {
21092 return true;
21093 }
21094 if (typeof session.updated !== "number" || session.updated <= 0 || !Array.isArray(session.desktops) || session.desktops.length === 0) {
21095 return false;
21096 }
21097 if (session.desktops.length > 1) {
21098 return true;
21099 }
21100 const onlyDesktop = session.desktops[0];
21101 if (onlyDesktop?.id && onlyDesktop.id !== "desktop-1") {
21102 return true;
21103 }
21104 return !!session.activeDesktop && session.activeDesktop !== "desktop-1";
21105 }
21106 async function restoreSession(manager2, config, desktopArea) {
21107 const rect = desktopArea.getBoundingClientRect();
21108 if (Array.isArray(config.session.desktops) && config.session.desktops.length > 0) {
21109 manager2.seedDesktops(
21110 config.session.desktops,
21111 config.session.activeDesktop || config.session.desktops[0].id
21112 );
21113 }
21114 for (const win of config.session.windows) {
21115 const clamped = clampGeometryToViewport(win, rect);
21116 const dockEntry = findDockEntryForUrl(win.url, config);
21117 const opened = await manager2.open({
21118 id: win.id,
21119 baseId: win.baseId || win.id,
21120 desktopId: win.desktopId,
21121 multi: !!dockEntry?.multi,
21122 url: win.url,
21123 // `dockEntry?.url` is the parent menu's landing page —
21124 // recover it so the synthetic "back to parent" tab in
21125 // the in-window strip points at the dock URL even when
21126 // the saved `win.url` is a sub-page (e.g. theme-install.php
21127 // under Appearance, or a deep wc-admin route under
21128 // WooCommerce). Without this the dedup check in
21129 // `dom.ts` sees the iframe URL match a submenu entry
21130 // and suppresses the parent tab — losing the only
21131 // affordance to navigate back.
21132 parentUrl: dockEntry?.url ?? win.url,
21133 title: win.title,
21134 icon: win.icon || "dashicons-admin-generic",
21135 x: clamped.x,
21136 y: clamped.y,
21137 width: clamped.width,
21138 height: clamped.height,
21139 initialState: win.state,
21140 submenu: dockEntry?.submenu
21141 });
21142 if (Array.isArray(win.externalTabs)) {
21143 for (const ext of win.externalTabs) {
21144 if (ext && typeof ext.url === "string" && ext.url !== "") {
21145 opened.addExternalTab(
21146 ext.url,
21147 typeof ext.label === "string" && ext.label !== "" ? ext.label : ext.url
21148 );
21149 }
21150 }
21151 }
21152 }
21153 if (config.session.focused) {
21154 const focused = manager2.getById(config.session.focused);
21155 if (focused) {
21156 manager2.focus(focused);
21157 }
21158 }
21159 }
21160 async function openCurrentPage(manager2, config) {
21161 if (tryNativeUrlRemap(config.currentPage)) {
21162 return;
21163 }
21164 const windowId = deriveWindowId(config.currentPage, config.adminUrl);
21165 const dockEntry = findDockEntryForUrl(config.currentPage, config);
21166 await manager2.open({
21167 id: windowId,
21168 baseId: windowId,
21169 multi: !!dockEntry?.multi,
21170 url: config.currentPage,
21171 parentUrl: dockEntry?.url ?? config.currentPage,
21172 title: config.currentTitle,
21173 icon: config.currentIcon,
21174 submenu: dockEntry?.submenu
21175 });
21176 }
21177 function shouldAutoOpenCurrentPage(inputs) {
21178 const suppress = inputs.fromPortal && !inputs.fromPortalIntent && (inputs.hasSession || !inputs.defaultEnabled || inputs.isNativeDefault);
21179 return !suppress;
21180 }
21181 function trackedFetch(manager2, input, requestInit, opts) {
21182 const finalInit = injectRestNonce(input, requestInit);
21183 const promise = window.fetch(input, finalInit);
21184 void promise.then(
21185 (res) => {
21186 if (res.status === 401 || res.status === 403) {
21187 let url;
21188 if (typeof input === "string") {
21189 url = input;
21190 } else if (input instanceof URL) {
21191 url = input.href;
21192 } else {
21193 url = input.url;
21194 }
21195 noteAuthFailure(res.status, url);
21196 }
21197 },
21198 () => {
21199 }
21200 );
21201 if (opts?.silent) {
21202 return promise;
21203 }
21204 let target2 = opts?.window;
21205 if (!target2 && opts?.windowId) {
21206 target2 = manager2.getById(opts.windowId) ?? null;
21207 }
21208 if (!target2) {
21209 target2 = manager2.getFocused();
21210 }
21211 if (target2 && typeof target2.trackActivity === "function") {
21212 void target2.trackActivity(promise).catch(() => {
21213 });
21214 }
21215 return promise;
21216 }
21217 const SESSION_SAVE_DEBOUNCE_MS = 500;
21218 function createSessionSaver(manager2, config) {
21219 let debounceTimer = null;
21220 let inFlight = false;
21221 const doSave = async () => {
21222 if (inFlight) {
21223 return;
21224 }
21225 const payload = manager2.snapshot();
21226 inFlight = true;
21227 try {
21228 await trackedFetch(
21229 manager2,
21230 config.sessionUrl,
21231 {
21232 method: "POST",
21233 credentials: "same-origin",
21234 headers: {
21235 "Content-Type": "application/json",
21236 "X-WP-Nonce": config.restNonce
21237 },
21238 body: JSON.stringify({ session: payload }),
21239 // Best-effort: we don't block the UI on persistence.
21240 keepalive: true
21241 },
21242 { silent: true }
21243 );
21244 } catch (err) {
21245 doAction(HOOKS.SHELL_ERROR, { scope: "session-save", error: err });
21246 } finally {
21247 inFlight = false;
21248 }
21249 };
21250 const flushImmediately = () => {
21251 if (debounceTimer !== null) {
21252 clearTimeout(debounceTimer);
21253 debounceTimer = null;
21254 }
21255 const payload = manager2.snapshot();
21256 const body = new Blob(
21257 [JSON.stringify({ session: payload })],
21258 { type: "application/json" }
21259 );
21260 const beaconUrl = config.sessionUrl + (config.sessionUrl.includes("?") ? "&" : "?") + "_wpnonce=" + encodeURIComponent(config.restNonce);
21261 if (navigator.sendBeacon && navigator.sendBeacon(beaconUrl, body)) {
21262 return;
21263 }
21264 void doSave();
21265 };
21266 const schedule = () => {
21267 if (debounceTimer !== null) {
21268 clearTimeout(debounceTimer);
21269 }
21270 debounceTimer = window.setTimeout(() => {
21271 debounceTimer = null;
21272 void doSave();
21273 }, SESSION_SAVE_DEBOUNCE_MS);
21274 };
21275 window.addEventListener("pagehide", flushImmediately);
21276 document.addEventListener("visibilitychange", () => {
21277 if (document.visibilityState === "hidden") {
21278 flushImmediately();
21279 }
21280 });
21281 return schedule;
21282 }
21283 const SHELL_RESIZE_DEBOUNCE_MS = 120;
21284 function wireSessionEvents(save) {
21285 document.addEventListener("desktop-mode-window-opened", save);
21286 document.addEventListener("desktop-mode-window-closed", save);
21287 document.addEventListener("desktop-mode-window-focused", save);
21288 document.addEventListener("desktop-mode-window-changed", save);
21289 addAction(HOOKS.DESKTOP_CREATED, "desktop-mode/session-save", save);
21290 addAction(HOOKS.DESKTOP_CLOSED, "desktop-mode/session-save", save);
21291 addAction(HOOKS.DESKTOP_SWITCHED, "desktop-mode/session-save", save);
21292 }
21293 function bindShellLifecycle() {
21294 const shellEl = document.getElementById("desktop-mode-shell");
21295 let resizeTimer = null;
21296 const fireShellResize = () => {
21297 resizeTimer = null;
21298 const rect = shellEl ? shellEl.getBoundingClientRect() : null;
21299 doAction(HOOKS.SHELL_RESIZED, {
21300 width: rect ? Math.round(rect.width) : window.innerWidth,
21301 height: rect ? Math.round(rect.height) : window.innerHeight
21302 });
21303 };
21304 window.addEventListener("resize", () => {
21305 if (resizeTimer !== null) {
21306 window.clearTimeout(resizeTimer);
21307 }
21308 resizeTimer = window.setTimeout(
21309 fireShellResize,
21310 SHELL_RESIZE_DEBOUNCE_MS
21311 );
21312 });
21313 document.addEventListener("visibilitychange", () => {
21314 doAction(HOOKS.SHELL_VISIBILITY, {
21315 state: document.hidden ? "hidden" : "visible"
21316 });
21317 });
21318 }
21319 function applyTileClasses(baseClasses, item, ctx) {
21320 const fullCtx = {
21321 rail: ctx.rail ?? "dock",
21322 orientation: ctx.orientation,
21323 dockId: ctx.dockId,
21324 container: ctx.container ?? document.body,
21325 item,
21326 isSystem: ctx.isSystem
21327 };
21328 return applyFilters(
21329 HOOKS.DOCK_TILE_CLASS,
21330 baseClasses,
21331 fullCtx
21332 );
21333 }
21334 function applyTileElement(tile2, item, ctx) {
21335 const fullCtx = {
21336 rail: ctx.rail ?? "dock",
21337 orientation: ctx.orientation,
21338 dockId: ctx.dockId,
21339 container: ctx.container ?? document.body,
21340 item,
21341 isSystem: ctx.isSystem
21342 };
21343 return applyFilters(
21344 HOOKS.DOCK_TILE_ELEMENT,
21345 tile2,
21346 fullCtx
21347 );
21348 }
21349 function applyTileTooltip(label, item, ctx) {
21350 const fullCtx = {
21351 rail: ctx.rail ?? "dock",
21352 orientation: ctx.orientation,
21353 dockId: ctx.dockId,
21354 container: ctx.container ?? document.body,
21355 item,
21356 isSystem: ctx.isSystem
21357 };
21358 return applyFilters(
21359 HOOKS.DOCK_TILE_TOOLTIP,
21360 label,
21361 fullCtx
21362 );
21363 }
21364 function dispatchTileRendered(el, item, ctx) {
21365 const fullCtx = {
21366 rail: ctx.rail ?? "dock",
21367 orientation: ctx.orientation,
21368 dockId: ctx.dockId,
21369 container: ctx.container ?? document.body,
21370 item,
21371 isSystem: ctx.isSystem
21372 };
21373 doAction(HOOKS.DOCK_TILE_RENDERED, { ...fullCtx, el });
21374 }
21375 const DEFAULT_DOCK_SELECTOR = [
21376 ".desktop-mode-dock",
21377 "#desktop-mode-dock",
21378 "#desktop-mode-side-dock",
21379 ".desktop-mode-dock__tooltip",
21380 ".desktop-mode-dock-submenu"
21381 ].join(",");
21382 const customSelectors = /* @__PURE__ */ new Set();
21383 function isDockElement(target2) {
21384 if (!target2 || typeof target2.closest !== "function") {
21385 return false;
21386 }
21387 const el = target2;
21388 if (el.closest(DEFAULT_DOCK_SELECTOR)) {
21389 return true;
21390 }
21391 for (const selector of customSelectors) {
21392 if (el.closest(selector)) {
21393 return true;
21394 }
21395 }
21396 return false;
21397 }
21398 function registerDockSelector(selector) {
21399 if (typeof selector !== "string" || selector.trim() === "") {
21400 return () => void 0;
21401 }
21402 customSelectors.add(selector);
21403 return () => {
21404 customSelectors.delete(selector);
21405 };
21406 }
21407 const states = /* @__PURE__ */ new Map();
21408 const INITIAL_ORIGIN = window.location.origin;
21409 function ensureState(windowId) {
21410 let s = states.get(windowId);
21411 if (!s) {
21412 s = {
21413 headers: /* @__PURE__ */ new Map(),
21414 observers: /* @__PURE__ */ new Set(),
21415 observeCount: 0,
21416 loadHandler: null,
21417 loadHandlerTarget: null
21418 };
21419 states.set(windowId, s);
21420 }
21421 ensureLoadHandler(windowId, s);
21422 return s;
21423 }
21424 function ensureLoadHandler(windowId, s) {
21425 const iframe = findIframe(windowId);
21426 if (!iframe) {
21427 return;
21428 }
21429 if (s.loadHandlerTarget === iframe && s.loadHandler) {
21430 return;
21431 }
21432 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
21433 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
21434 }
21435 if (typeof iframe.addEventListener !== "function") {
21436 return;
21437 }
21438 const handler = () => {
21439 queueMicrotask(() => pushInstrumentation(windowId));
21440 };
21441 iframe.addEventListener("load", handler);
21442 s.loadHandler = handler;
21443 s.loadHandlerTarget = iframe;
21444 }
21445 function detachLoadHandler(s) {
21446 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
21447 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
21448 }
21449 s.loadHandler = null;
21450 s.loadHandlerTarget = null;
21451 }
21452 function findIframe(windowId) {
21453 const wpd = window.wp?.desktop?.windowManager;
21454 if (wpd && typeof wpd.getById === "function") {
21455 const win = wpd.getById(windowId);
21456 if (win?.iframe) {
21457 return win.iframe;
21458 }
21459 if (win?.element) {
21460 const synth = win.element.querySelector("iframe");
21461 if (synth) {
21462 return synth;
21463 }
21464 }
21465 }
21466 const fallback = document.getElementById(`wp-window-${windowId}`);
21467 return fallback?.querySelector("iframe") ?? null;
21468 }
21469 function snapshotHeaders(s) {
21470 const out = {};
21471 for (const [name, contributions] of s.headers) {
21472 const parts = [];
21473 for (const c of contributions) {
21474 let v;
21475 try {
21476 v = typeof c.value === "function" ? c.value() : c.value;
21477 } catch {
21478 continue;
21479 }
21480 if (typeof v === "string" && v !== "") {
21481 parts.push(v);
21482 }
21483 }
21484 if (parts.length > 0) {
21485 out[name] = parts.join(", ");
21486 }
21487 }
21488 return out;
21489 }
21490 function pushInstrumentation(windowId) {
21491 const iframe = findIframe(windowId);
21492 if (!iframe || !iframe.contentWindow) {
21493 return;
21494 }
21495 const s = states.get(windowId);
21496 const headers = s ? snapshotHeaders(s) : {};
21497 const observe = !!s && s.observeCount > 0;
21498 try {
21499 iframe.contentWindow.postMessage(
21500 {
21501 type: "desktop-mode-instrument-set",
21502 headers,
21503 observe
21504 },
21505 INITIAL_ORIGIN
21506 );
21507 } catch {
21508 }
21509 }
21510 addAction(HOOKS.IFRAME_READY, "desktop-mode/devtools/replay", (payload) => {
21511 const p = payload;
21512 if (p && typeof p.windowId === "string" && states.has(p.windowId)) {
21513 pushInstrumentation(p.windowId);
21514 }
21515 });
21516 addAction(
21517 HOOKS.IFRAME_NETWORK_COMPLETED,
21518 "desktop-mode/devtools/dispatch",
21519 (payload) => {
21520 const p = payload;
21521 if (!p || typeof p.windowId !== "string") {
21522 return;
21523 }
21524 const s = states.get(p.windowId);
21525 if (!s) {
21526 return;
21527 }
21528 for (const cb of s.observers) {
21529 try {
21530 cb(p);
21531 } catch {
21532 }
21533 }
21534 }
21535 );
21536 const sessions = /* @__PURE__ */ new Map();
21537 const POLL_INTERVAL_MS = 1e3;
21538 function pollOnce(sessionId, restUrl2, restNonce) {
21539 const sp = sessions.get(sessionId);
21540 if (!sp || sp.inflight) {
21541 return;
21542 }
21543 sp.inflight = true;
21544 const u = new URL(restUrl2 + "desktop-mode/v1/debug", window.location.origin);
21545 u.searchParams.set("sessionId", sessionId);
21546 u.searchParams.set("since", String(sp.cursor));
21547 for (const ch of sp.channels.keys()) {
21548 u.searchParams.append("channels[]", ch);
21549 }
21550 const url = u.toString();
21551 fetch(url, {
21552 credentials: "same-origin",
21553 headers: { "X-WP-Nonce": restNonce }
21554 }).then((r) => r.ok ? r.json() : { events: [], cursor: sp.cursor }).then((body) => {
21555 sp.inflight = false;
21556 if (!sessions.has(sessionId)) {
21557 return;
21558 }
21559 if (typeof body.cursor === "number") {
21560 sp.cursor = body.cursor;
21561 }
21562 for (const ev of body.events || []) {
21563 const bucket2 = sp.channels.get(ev.channel);
21564 if (!bucket2) {
21565 continue;
21566 }
21567 for (const cb of bucket2) {
21568 try {
21569 cb(ev);
21570 } catch {
21571 }
21572 }
21573 }
21574 }).catch(() => {
21575 sp.inflight = false;
21576 }).finally(() => {
21577 const stillThere = sessions.get(sessionId);
21578 if (stillThere && stillThere.channels.size > 0) {
21579 stillThere.timer = setTimeout(
21580 () => pollOnce(sessionId, restUrl2, restNonce),
21581 POLL_INTERVAL_MS
21582 );
21583 }
21584 });
21585 }
21586 function getRestEndpoint() {
21587 const cfg = window.desktopModeConfig;
21588 if (!cfg || !cfg.restUrl || !cfg.restNonce) {
21589 return null;
21590 }
21591 return { restUrl: cfg.restUrl, restNonce: cfg.restNonce };
21592 }
21593 function dispatchLocal(sessionId, ev) {
21594 const sp = sessions.get(sessionId);
21595 if (!sp) {
21596 return;
21597 }
21598 const bucket2 = sp.channels.get(ev.channel);
21599 if (!bucket2) {
21600 return;
21601 }
21602 for (const cb of bucket2) {
21603 try {
21604 cb(ev);
21605 } catch {
21606 }
21607 }
21608 }
21609 let _localEventCounter = 0;
21610 const debugBus = {
21611 startSession() {
21612 const cryptoApi = window.crypto;
21613 if (cryptoApi && typeof cryptoApi.randomUUID === "function") {
21614 return cryptoApi.randomUUID();
21615 }
21616 return "wpdbg-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
21617 },
21618 publish(sessionId, channel, payload) {
21619 dispatchLocal(sessionId, {
21620 id: ++_localEventCounter,
21621 t: Date.now(),
21622 channel,
21623 payload
21624 });
21625 },
21626 subscribe(sessionId, channel, cb) {
21627 let sp = sessions.get(sessionId);
21628 const startedFresh = !sp;
21629 if (!sp) {
21630 sp = {
21631 channels: /* @__PURE__ */ new Map(),
21632 cursor: 0,
21633 timer: null,
21634 inflight: false
21635 };
21636 sessions.set(sessionId, sp);
21637 }
21638 let bucket2 = sp.channels.get(channel);
21639 if (!bucket2) {
21640 bucket2 = /* @__PURE__ */ new Set();
21641 sp.channels.set(channel, bucket2);
21642 }
21643 bucket2.add(cb);
21644 if (startedFresh) {
21645 const ep = getRestEndpoint();
21646 if (ep) {
21647 pollOnce(sessionId, ep.restUrl, ep.restNonce);
21648 }
21649 }
21650 return () => {
21651 const cur = sessions.get(sessionId);
21652 if (!cur) {
21653 return;
21654 }
21655 const b = cur.channels.get(channel);
21656 if (b) {
21657 b.delete(cb);
21658 if (b.size === 0) {
21659 cur.channels.delete(channel);
21660 }
21661 }
21662 if (cur.channels.size === 0) {
21663 if (cur.timer) {
21664 clearTimeout(cur.timer);
21665 }
21666 sessions.delete(sessionId);
21667 }
21668 };
21669 }
21670 };
21671 const devtools = {
21672 addRequestHeader(windowId, name, value) {
21673 if (typeof windowId !== "string" || windowId === "") {
21674 return () => {
21675 };
21676 }
21677 if (typeof name !== "string" || name === "") {
21678 return () => {
21679 };
21680 }
21681 const s = ensureState(windowId);
21682 const contribution = { value };
21683 let bucket2 = s.headers.get(name);
21684 if (!bucket2) {
21685 bucket2 = [];
21686 s.headers.set(name, bucket2);
21687 }
21688 bucket2.push(contribution);
21689 pushInstrumentation(windowId);
21690 return () => {
21691 const cur = states.get(windowId);
21692 if (!cur) {
21693 return;
21694 }
21695 const b = cur.headers.get(name);
21696 if (!b) {
21697 return;
21698 }
21699 const i = b.indexOf(contribution);
21700 if (i >= 0) {
21701 b.splice(i, 1);
21702 }
21703 if (b.length === 0) {
21704 cur.headers.delete(name);
21705 }
21706 pushInstrumentation(windowId);
21707 gcWindowState(windowId);
21708 };
21709 },
21710 onRequest(windowId, cb, opts) {
21711 if (typeof windowId !== "string" || windowId === "") {
21712 return () => {
21713 };
21714 }
21715 if (typeof cb !== "function") {
21716 return () => {
21717 };
21718 }
21719 const s = ensureState(windowId);
21720 s.observers.add(cb);
21721 const wantsObserve = !!opts?.observe;
21722 if (wantsObserve) {
21723 s.observeCount++;
21724 pushInstrumentation(windowId);
21725 }
21726 return () => {
21727 const cur = states.get(windowId);
21728 if (!cur) {
21729 return;
21730 }
21731 cur.observers.delete(cb);
21732 if (wantsObserve) {
21733 cur.observeCount = Math.max(0, cur.observeCount - 1);
21734 pushInstrumentation(windowId);
21735 }
21736 gcWindowState(windowId);
21737 };
21738 },
21739 reloadWithDebugSession(windowId, sessionId, opts) {
21740 if (typeof windowId !== "string" || windowId === "" || typeof sessionId !== "string" || sessionId === "") {
21741 return null;
21742 }
21743 const iframe = findIframe(windowId);
21744 if (!iframe) {
21745 return null;
21746 }
21747 const headerName = opts?.headerName || "X-WP-Debug-Session";
21748 const queryArg = opts?.queryArg || "wp_debug_session";
21749 const stopHeader = devtools.addRequestHeader(windowId, headerName, sessionId);
21750 try {
21751 const currentSrc = iframe.getAttribute("src") || iframe.src || "";
21752 const u = new URL(currentSrc, window.location.origin);
21753 u.searchParams.set(queryArg, sessionId);
21754 iframe.src = u.toString();
21755 } catch {
21756 }
21757 return {
21758 dispose: () => {
21759 stopHeader();
21760 }
21761 };
21762 },
21763 debug: debugBus
21764 };
21765 function gcWindowState(windowId) {
21766 const s = states.get(windowId);
21767 if (!s) {
21768 return;
21769 }
21770 if (s.headers.size === 0 && s.observers.size === 0) {
21771 detachLoadHandler(s);
21772 states.delete(windowId);
21773 }
21774 }
21775 async function wpdConfirm(options) {
21776 await ensureShellOverlaysLoaded(shellOverlaysBundleUrl());
21777 return new Promise((resolve2) => {
21778 const dialog2 = document.createElement("wpd-confirm-dialog");
21779 dialog2.setAttribute("open", "");
21780 if (options.title) {
21781 dialog2.setAttribute("title", options.title);
21782 }
21783 dialog2.setAttribute("message", options.message);
21784 if (options.confirmLabel) {
21785 dialog2.setAttribute("confirm-label", options.confirmLabel);
21786 }
21787 if (options.cancelLabel) {
21788 dialog2.setAttribute("cancel-label", options.cancelLabel);
21789 }
21790 if (options.danger) {
21791 dialog2.setAttribute("danger", "");
21792 }
21793 if (options.hideCancel) {
21794 dialog2.setAttribute("hide-cancel", "");
21795 }
21796 if (options.dismissable) {
21797 dialog2.setAttribute("dismissable", "");
21798 }
21799 const cleanup = (ok) => {
21800 dialog2.remove();
21801 resolve2(ok);
21802 };
21803 dialog2.addEventListener("wpd-confirm", () => cleanup(true));
21804 dialog2.addEventListener("wpd-cancel", () => cleanup(false));
21805 document.body.appendChild(dialog2);
21806 const inner = dialog2.shadowRoot?.querySelector(".dialog");
21807 (inner ?? dialog2).focus?.();
21808 });
21809 }
21810 function collectWallpaperSurfaces(manager2) {
21811 const seed2 = [];
21812 for (const w of manager2.getVisibleRects()) {
21813 if (w.state === "minimized") {
21814 continue;
21815 }
21816 if (w.element.offsetParent === null) {
21817 continue;
21818 }
21819 const r = w.element.getBoundingClientRect();
21820 seed2.push({
21821 id: `window:${w.windowId}`,
21822 kind: "window",
21823 rect: rectFromDom(r),
21824 face: "top",
21825 element: w.element
21826 });
21827 }
21828 const shellEl = document.getElementById("desktop-mode-shell");
21829 if (shellEl) {
21830 const r = shellEl.getBoundingClientRect();
21831 seed2.push({
21832 id: "shell:floor",
21833 kind: "shell",
21834 rect: {
21835 x: r.left,
21836 y: r.bottom - 1,
21837 width: r.width,
21838 height: 1
21839 },
21840 face: "top",
21841 element: shellEl
21842 });
21843 }
21844 const dockEls = document.querySelectorAll(
21845 ".desktop-mode-dock"
21846 );
21847 let dockIndex = 0;
21848 for (const dockEl of Array.from(dockEls)) {
21849 const r = dockEl.getBoundingClientRect();
21850 if (r.width <= 0 || r.height <= 0) {
21851 continue;
21852 }
21853 const placement = dockEl.getAttribute("data-desktop-mode-dock-placement") ?? "bottom";
21854 const id = dockIndex === 0 ? "dock:edge" : `dock:edge:${dockIndex}`;
21855 dockIndex++;
21856 if (placement === "bottom") {
21857 seed2.push({
21858 id,
21859 kind: "dock",
21860 rect: { x: r.left, y: r.top, width: r.width, height: 1 },
21861 face: "top",
21862 element: dockEl
21863 });
21864 } else if (placement === "right") {
21865 seed2.push({
21866 id,
21867 kind: "dock",
21868 rect: { x: r.left, y: r.top, width: 1, height: r.height },
21869 face: "left",
21870 element: dockEl
21871 });
21872 } else {
21873 seed2.push({
21874 id,
21875 kind: "dock",
21876 rect: {
21877 x: r.right - 1,
21878 y: r.top,
21879 width: 1,
21880 height: r.height
21881 },
21882 face: "right",
21883 element: dockEl
21884 });
21885 }
21886 }
21887 const widgetCards = document.querySelectorAll(
21888 ".desktop-mode-widgets__card"
21889 );
21890 let widgetIndex = 0;
21891 widgetCards.forEach((card) => {
21892 const r = card.getBoundingClientRect();
21893 if (r.width === 0 && r.height === 0) {
21894 return;
21895 }
21896 const id = card.dataset.widgetId ?? String(widgetIndex++);
21897 seed2.push({
21898 id: `widget:${id}`,
21899 kind: "widget",
21900 rect: rectFromDom(r),
21901 face: "top",
21902 element: card
21903 });
21904 });
21905 const filtered = applyFilters(HOOKS.WALLPAPER_SURFACES, seed2);
21906 return Array.isArray(filtered) ? filtered : seed2;
21907 }
21908 function rectFromDom(r) {
21909 return {
21910 x: r.left,
21911 y: r.top,
21912 width: r.width,
21913 height: r.height
21914 };
21915 }
21916 const NODE_KEY_PROP = "__desktop_modeKeyedListKey";
21917 const NODE_DATA_PROP = "__desktop_modeKeyedListData";
21918 function getHostState(host) {
21919 const cached = host.__desktop_modeKeyedList;
21920 if (cached) {
21921 return cached;
21922 }
21923 const fresh = { byKey: /* @__PURE__ */ new Map() };
21924 host.__desktop_modeKeyedList = fresh;
21925 return fresh;
21926 }
21927 function renderKeyedList(host, items, opts) {
21928 const state2 = getHostState(host);
21929 const prev = state2.byKey;
21930 const next = /* @__PURE__ */ new Map();
21931 const ordered = [];
21932 const seenKeys = /* @__PURE__ */ new Set();
21933 for (const item of items) {
21934 const key = String(opts.keyOf(item));
21935 if (seenKeys.has(key)) {
21936 console.warn(
21937 "[desktop-mode/keyed-list] duplicate key — only the last item with this key will render:",
21938 key
21939 );
21940 }
21941 seenKeys.add(key);
21942 const reused = prev.get(key);
21943 if (reused) {
21944 const prevData = reused.data;
21945 opts.updateItem?.(reused.el, item, prevData);
21946 reused.data = item;
21947 next.set(key, reused);
21948 ordered.push(reused.el);
21949 continue;
21950 }
21951 const el = opts.buildItem(item);
21952 el[NODE_KEY_PROP] = key;
21953 el[NODE_DATA_PROP] = item;
21954 next.set(key, { el, data: item });
21955 ordered.push(el);
21956 }
21957 for (const [key, entry] of prev) {
21958 if (!next.has(key)) {
21959 entry.el.remove();
21960 }
21961 }
21962 for (let i = 0; i < ordered.length; i++) {
21963 const desired = ordered[i];
21964 const live = host.children[i];
21965 if (live === desired) {
21966 continue;
21967 }
21968 host.insertBefore(desired, live ?? null);
21969 }
21970 state2.byKey = next;
21971 }
21972 function clearKeyedList(host) {
21973 const cached = host.__desktop_modeKeyedList;
21974 if (!cached) {
21975 return;
21976 }
21977 for (const entry of cached.byKey.values()) {
21978 entry.el.remove();
21979 }
21980 cached.byKey.clear();
21981 delete host.__desktop_modeKeyedList;
21982 }
21983 function createInfiniteList(options) {
21984 const {
21985 root,
21986 fetchPage,
21987 getId,
21988 renderItem,
21989 rootMargin = "200px",
21990 initialCursor = null,
21991 onLoadingChange = () => void 0,
21992 onError = (err) => {
21993 if (typeof console !== "undefined") {
21994 console.error("[desktop-mode] createInfiniteList:", err);
21995 }
21996 }
21997 } = options;
21998 let sentinel = options.sentinel ?? null;
21999 if (!sentinel) {
22000 sentinel = document.createElement("div");
22001 sentinel.dataset.wpdInfiniteListSentinel = "";
22002 sentinel.style.height = "1px";
22003 root.appendChild(sentinel);
22004 }
22005 const seen = /* @__PURE__ */ new Set();
22006 let cursor = initialCursor;
22007 let hasMoreInternal = true;
22008 let loading = false;
22009 let controller = null;
22010 let renderedCount = 0;
22011 let destroyed = false;
22012 let observer = null;
22013 const setLoading = (next) => {
22014 if (loading === next) {
22015 return;
22016 }
22017 loading = next;
22018 try {
22019 onLoadingChange(next);
22020 } catch (err) {
22021 onError(err);
22022 }
22023 };
22024 const detachObserver = () => {
22025 if (observer) {
22026 observer.disconnect();
22027 observer = null;
22028 }
22029 };
22030 const ensureObserver = () => {
22031 if (observer || !sentinel || destroyed) {
22032 return;
22033 }
22034 observer = new IntersectionObserver(
22035 (entries) => {
22036 for (const entry of entries) {
22037 if (entry.isIntersecting) {
22038 void loadMore();
22039 }
22040 }
22041 },
22042 { rootMargin }
22043 );
22044 observer.observe(sentinel);
22045 };
22046 const loadMore = async () => {
22047 if (destroyed || loading || !hasMoreInternal) {
22048 return;
22049 }
22050 setLoading(true);
22051 controller = new AbortController();
22052 const localController = controller;
22053 try {
22054 const page = await fetchPage(cursor, localController.signal);
22055 if (destroyed || localController !== controller) {
22056 return;
22057 }
22058 let appended = 0;
22059 const frag = document.createDocumentFragment();
22060 for (const item of page.items ?? []) {
22061 const key = String(getId(item));
22062 if (seen.has(key)) {
22063 continue;
22064 }
22065 seen.add(key);
22066 const el = renderItem(item, renderedCount + appended);
22067 frag.appendChild(el);
22068 appended++;
22069 }
22070 if (appended > 0) {
22071 if (sentinel && sentinel.parentNode === root) {
22072 root.insertBefore(frag, sentinel);
22073 } else {
22074 root.appendChild(frag);
22075 }
22076 renderedCount += appended;
22077 }
22078 cursor = page.nextCursor ?? null;
22079 if (!cursor) {
22080 hasMoreInternal = false;
22081 detachObserver();
22082 }
22083 } catch (err) {
22084 if (err?.name === "AbortError") {
22085 return;
22086 }
22087 onError(err);
22088 } finally {
22089 if (localController === controller) {
22090 setLoading(false);
22091 controller = null;
22092 }
22093 }
22094 };
22095 const reset = () => {
22096 if (destroyed) {
22097 return;
22098 }
22099 controller?.abort();
22100 controller = null;
22101 seen.clear();
22102 cursor = initialCursor;
22103 hasMoreInternal = true;
22104 renderedCount = 0;
22105 const sentinelInRoot = sentinel && sentinel.parentNode === root;
22106 while (root.firstChild) {
22107 root.removeChild(root.firstChild);
22108 }
22109 if (sentinelInRoot && sentinel) {
22110 root.appendChild(sentinel);
22111 }
22112 setLoading(false);
22113 ensureObserver();
22114 void loadMore();
22115 };
22116 const destroy = () => {
22117 if (destroyed) {
22118 return;
22119 }
22120 destroyed = true;
22121 detachObserver();
22122 controller?.abort();
22123 controller = null;
22124 if (!options.sentinel && sentinel && sentinel.parentNode === root) {
22125 root.removeChild(sentinel);
22126 }
22127 sentinel = null;
22128 setLoading(false);
22129 };
22130 ensureObserver();
22131 void loadMore();
22132 return {
22133 reset,
22134 loadMore,
22135 hasMore: () => hasMoreInternal,
22136 isLoading: () => loading,
22137 destroy
22138 };
22139 }
22140 const POPUP_DEFAULT_WIDTH = 520;
22141 const POPUP_DEFAULT_HEIGHT = 720;
22142 const POPUP_CLOSE_POLL_MS = 500;
22143 function startOAuth(service, options = {}) {
22144 if (typeof service !== "string" || service === "") {
22145 return Promise.reject(
22146 new Error("[desktop-mode] startOAuth requires a non-empty service slug.")
22147 );
22148 }
22149 const restRoot2 = readRestRoot$1();
22150 const restNonce = readRestNonce$1();
22151 return trackedFetch$1(
22152 joinRestUrl(restRoot2, "desktop-mode/v1/oauth/start"),
22153 {
22154 method: "POST",
22155 headers: {
22156 "Content-Type": "application/json",
22157 "X-WP-Nonce": restNonce ?? ""
22158 },
22159 body: JSON.stringify({ service })
22160 },
22161 { source: "desktop-mode/oauth-start" }
22162 ).then(async (res) => {
22163 if (!res.ok) {
22164 const text = await res.text().catch(() => "");
22165 throw new Error(
22166 `[desktop-mode] OAuth start failed (${res.status}): ${text}`
22167 );
22168 }
22169 return await res.json();
22170 }).then((startBody) => openPopupAndWait(startBody, service, options));
22171 }
22172 function openPopupAndWait(body, service, options) {
22173 return new Promise((resolve2, reject) => {
22174 const width = options.width ?? POPUP_DEFAULT_WIDTH;
22175 const height = options.height ?? POPUP_DEFAULT_HEIGHT;
22176 const left = Math.max(0, Math.floor((window.screen.width - width) / 2));
22177 const top = Math.max(0, Math.floor((window.screen.height - height) / 2));
22178 const features = [
22179 `width=${width}`,
22180 `height=${height}`,
22181 `left=${left}`,
22182 `top=${top}`,
22183 "menubar=no",
22184 "toolbar=no",
22185 "location=yes",
22186 "status=no",
22187 "resizable=yes",
22188 "scrollbars=yes"
22189 ].join(",");
22190 const popup = window.open(
22191 body.authorize_url,
22192 `desktop-mode-oauth-${service}`,
22193 features
22194 );
22195 if (!popup) {
22196 reject(
22197 new Error(
22198 "[desktop-mode] OAuth popup blocked. Tell users to allow popups for this site."
22199 )
22200 );
22201 return;
22202 }
22203 const expectedOrigin = window.location.origin;
22204 let pollTimer = null;
22205 let detached = false;
22206 const cleanup = () => {
22207 if (detached) {
22208 return;
22209 }
22210 detached = true;
22211 window.removeEventListener("message", onMessage);
22212 if (pollTimer !== null) {
22213 window.clearInterval(pollTimer);
22214 pollTimer = null;
22215 }
22216 };
22217 const onMessage = (e) => {
22218 if (e.origin !== expectedOrigin) {
22219 return;
22220 }
22221 const data = e.data;
22222 if (!data || data.type !== "desktop-mode-oauth-callback") {
22223 return;
22224 }
22225 const payload = data.payload;
22226 cleanup();
22227 if (payload && payload.ok) {
22228 resolve2(payload);
22229 } else {
22230 const reason = payload?.reason ?? "unknown";
22231 const message = payload?.message ?? "OAuth flow failed";
22232 const err = new Error(
22233 `[desktop-mode] startOAuth(${service}) failed: ${reason} — ${message}`
22234 );
22235 err.cause = payload;
22236 reject(err);
22237 }
22238 };
22239 window.addEventListener("message", onMessage);
22240 pollTimer = window.setInterval(() => {
22241 if (popup.closed) {
22242 cleanup();
22243 reject(
22244 new Error(
22245 `[desktop-mode] startOAuth(${service}) cancelled — popup closed before completing.`
22246 )
22247 );
22248 }
22249 }, POPUP_CLOSE_POLL_MS);
22250 });
22251 }
22252 function readDesktopConfig() {
22253 return window.desktopModeConfig ?? {};
22254 }
22255 function readRestRoot$1() {
22256 const root = readDesktopConfig().restRoot;
22257 if (typeof root === "string" && root !== "") {
22258 return root;
22259 }
22260 return `${window.location.origin}/wp-json/`;
22261 }
22262 function readRestNonce$1() {
22263 const nonce = readDesktopConfig().restNonce;
22264 return typeof nonce === "string" && nonce !== "" ? nonce : null;
22265 }
22266 const gamesApi = {
22267 register: register$1,
22268 unregister: unregister$1,
22269 list: all$1,
22270 get: get$1,
22271 subscribe: subscribe$3,
22272 launch: launchGame,
22273 getPlaytime: () => fetchPlaytime().then((res) => res.playtime)
22274 };
22275 const RESERVED_NAMESPACE_KEYS = /* @__PURE__ */ new Set([
22276 "windowManager",
22277 "dock",
22278 "sideDock",
22279 "taskbar",
22280 "desktopLayout",
22281 "icons",
22282 "files",
22283 "confirm",
22284 "saveSession",
22285 "hooks",
22286 "HOOKS",
22287 "isActive",
22288 "registerWallpaper",
22289 "registerWidget",
22290 "widgetLayer",
22291 "widgets",
22292 "registerSystemTile",
22293 "registerWindow",
22294 "openWindow",
22295 "openNewWindow",
22296 "cloneTemplate",
22297 "onWindow",
22298 "createInfiniteList",
22299 "startOAuth",
22300 "repaintLoadingOverlays",
22301 "loadVendorScript",
22302 "getWallpaperSurfaces",
22303 "wallpaper",
22304 "games",
22305 "registerModule",
22306 "loadModules",
22307 "whenReady",
22308 "ready",
22309 "isReady",
22310 "setDefaultWindow",
22311 "refreshMenu",
22312 "config",
22313 "ai",
22314 "dragBridge",
22315 "dragManager",
22316 "registerCommand",
22317 "unregisterCommand",
22318 "listCommands",
22319 "registerDestructiveAdminAction",
22320 "unregisterDestructiveAdminAction",
22321 "listDestructiveAdminActions",
22322 "registerSettingsTab",
22323 "unregisterSettingsTab",
22324 "listSettingsTabs",
22325 "registerDockRailRenderer",
22326 "unregisterDockRailRenderer",
22327 "listDockRailRenderers",
22328 "openOsSettings",
22329 "getOsSettings",
22330 "subscribeOsSettings",
22331 "updateOsSettings",
22332 "deriveWindowId",
22333 "listSystemTiles",
22334 "getSystemTile",
22335 "getMenuItems",
22336 "renderIcon",
22337 "applyTileClasses",
22338 "applyTileElement",
22339 "applyTileTooltip",
22340 "dispatchTileRendered",
22341 "isDockElement",
22342 "registerDockSelector",
22343 "registerTitleBarButton",
22344 "unregisterTitleBarButton",
22345 "listTitleBarButtons",
22346 "registerUnfocusEffect",
22347 "unregisterUnfocusEffect",
22348 "listUnfocusEffects",
22349 "relations",
22350 "registerWindowLinkRenderer",
22351 "unregisterWindowLinkRenderer",
22352 "listWindowLinkRenderers",
22353 "registerWindowTheme",
22354 "unregisterWindowTheme",
22355 "listWindowThemes",
22356 "applyWindowTheme",
22357 "registerWindowControl",
22358 "unregisterWindowControl",
22359 "listWindowControls",
22360 "applyWindowControls",
22361 "registerWindowSlot",
22362 "unregisterWindowSlot",
22363 "listWindowSlots",
22364 "applyWindowSlot",
22365 "registerWindowNotice",
22366 "unregisterWindowNotice",
22367 "listWindowNotices",
22368 "dismissWindowNotice",
22369 "undismissWindowNotice",
22370 "registerWindowChrome",
22371 "unregisterWindowChrome",
22372 "listWindowChromes",
22373 "applyWindowChrome",
22374 "connect",
22375 "getConnection",
22376 "broadcast",
22377 "subscribe",
22378 "registerPalette",
22379 "unregisterPalette",
22380 "listPalettes",
22381 "openPalette",
22382 "devtools",
22383 "createSharedStore",
22384 "presence",
22385 "activity",
22386 "heartbeat",
22387 "showToast",
22388 "renderKeyedList",
22389 "clearKeyedList",
22390 "registerNamespace",
22391 "notify",
22392 "pwa",
22393 "getWindowConfig",
22394 "debug",
22395 "fetch"
22396 ]);
22397 function buildPublicApi(deps2) {
22398 const {
22399 manager: manager2,
22400 dock,
22401 layoutDispatcher,
22402 osSettings,
22403 iconsApi: iconsApi2,
22404 filesApi: filesApi2,
22405 saveSession,
22406 widgetLayer,
22407 registerWindow,
22408 openWindowById,
22409 openNewWindowById,
22410 placeSystemTile,
22411 setDefaultWindow,
22412 refreshMenu,
22413 openOsSettings,
22414 aiAssistant,
22415 dragBridge,
22416 dragManager,
22417 connect,
22418 getConnection,
22419 wallpaperSuspend,
22420 config
22421 } = deps2;
22422 const desktopApi = {
22423 windowManager: manager2,
22424 dock,
22425 sideDock: layoutDispatcher?.getSide() ?? null,
22426 desktopLayout: osSettings.getOsSettingsSnapshot().desktopLayout,
22427 icons: iconsApi2,
22428 files: filesApi2,
22429 confirm: wpdConfirm,
22430 saveSession,
22431 hooks: rawHooks(),
22432 HOOKS,
22433 isActive: () => !!document.getElementById("desktop-mode-shell"),
22434 registerWallpaper: (def) => {
22435 register$3(def);
22436 osSettings.apply();
22437 },
22438 registerWidget: (def) => {
22439 register(def);
22440 },
22441 widgetLayer,
22442 widgets: {
22443 redock: (id) => {
22444 widgetLayer?.redock(id);
22445 }
22446 },
22447 loadVendorScript,
22448 getWallpaperSurfaces: () => collectWallpaperSurfaces(manager2),
22449 wallpaper: wallpaperSuspend,
22450 games: gamesApi,
22451 registerWindow,
22452 openWindow: openWindowById,
22453 openNewWindow: openNewWindowById,
22454 fetch: (input, requestInit, opts) => trackedFetch(manager2, input, requestInit, opts),
22455 repaintLoadingOverlays,
22456 cloneTemplate,
22457 onWindow,
22458 createInfiniteList,
22459 startOAuth,
22460 registerSystemTile: (item) => {
22461 placeSystemTile(item);
22462 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: item.id });
22463 },
22464 registerModule,
22465 loadModules,
22466 whenReady,
22467 ready: whenReady,
22468 isReady,
22469 setDefaultWindow,
22470 refreshMenu,
22471 config,
22472 ai: aiAssistant,
22473 dragBridge,
22474 dragManager,
22475 registerCommand,
22476 unregisterCommand,
22477 listCommands,
22478 registerDestructiveAdminAction,
22479 unregisterDestructiveAdminAction,
22480 listDestructiveAdminActions,
22481 registerSettingsTab,
22482 unregisterSettingsTab,
22483 listSettingsTabs,
22484 registerDockRailRenderer: register$2,
22485 unregisterDockRailRenderer: unregister$2,
22486 listDockRailRenderers: list,
22487 openOsSettings,
22488 getOsSettings: () => osSettings.getOsSettingsSnapshot(),
22489 subscribeOsSettings: (cb) => osSettings.subscribeOsSettings(cb),
22490 updateOsSettings: (patch, opts = {}) => {
22491 if (typeof patch.wallpaper === "string") {
22492 osSettings.state.wallpaper = patch.wallpaper;
22493 }
22494 if (typeof patch.accent === "string") {
22495 osSettings.state.accent = patch.accent;
22496 }
22497 if (typeof patch.dockSize === "string") {
22498 osSettings.state.dockSize = patch.dockSize;
22499 }
22500 if (typeof patch.desktopLayout === "string") {
22501 osSettings.state.desktopLayout = patch.desktopLayout;
22502 }
22503 if (typeof patch.dockRailRenderer === "string") {
22504 osSettings.state.dockRailRenderer = patch.dockRailRenderer;
22505 }
22506 if (typeof patch.windowLinkRenderer === "string") {
22507 osSettings.state.windowLinkRenderer = patch.windowLinkRenderer;
22508 }
22509 if (patch.windowLinkVisibility === "focus" || patch.windowLinkVisibility === "always" || patch.windowLinkVisibility === "off") {
22510 osSettings.state.windowLinkVisibility = patch.windowLinkVisibility;
22511 }
22512 if (typeof patch.windowLinksEnabled === "boolean") {
22513 osSettings.state.windowLinksEnabled = patch.windowLinksEnabled;
22514 }
22515 if (typeof patch.windowLinkRaiseOnFocus === "boolean") {
22516 osSettings.state.windowLinkRaiseOnFocus = patch.windowLinkRaiseOnFocus;
22517 }
22518 if (typeof patch.windowLinkHighlight === "boolean") {
22519 osSettings.state.windowLinkHighlight = patch.windowLinkHighlight;
22520 }
22521 if (patch.ai && typeof patch.ai === "object") {
22522 osSettings.state.ai = { ...osSettings.state.ai, ...patch.ai };
22523 }
22524 if (typeof patch.nativePostsEnabled === "boolean") {
22525 osSettings.state.nativePostsEnabled = patch.nativePostsEnabled;
22526 }
22527 if (typeof patch.nativePagesEnabled === "boolean") {
22528 osSettings.state.nativePagesEnabled = patch.nativePagesEnabled;
22529 }
22530 if (typeof patch.nativeUsersEnabled === "boolean") {
22531 osSettings.state.nativeUsersEnabled = patch.nativeUsersEnabled;
22532 }
22533 if (typeof patch.nativePluginsEnabled === "boolean") {
22534 osSettings.state.nativePluginsEnabled = patch.nativePluginsEnabled;
22535 }
22536 if (typeof patch.nativeCommentsEnabled === "boolean") {
22537 osSettings.state.nativeCommentsEnabled = patch.nativeCommentsEnabled;
22538 }
22539 if (typeof patch.foldersSharingEnabled === "boolean") {
22540 osSettings.state.foldersSharingEnabled = patch.foldersSharingEnabled;
22541 }
22542 if (typeof patch.developerModeEnabled === "boolean") {
22543 osSettings.state.developerModeEnabled = patch.developerModeEnabled;
22544 }
22545 if (Array.isArray(patch.nativePostsHiddenColumns)) {
22546 osSettings.state.nativePostsHiddenColumns = patch.nativePostsHiddenColumns.filter(
22547 (v) => typeof v === "string" && v !== ""
22548 ).slice(0, 32);
22549 }
22550 if (patch.itemVisibility && typeof patch.itemVisibility === "object") {
22551 const allowed = ["both", "dock", "desktop", "hidden"];
22552 const next = {};
22553 for (const [k, v] of Object.entries(
22554 patch.itemVisibility
22555 )) {
22556 if (typeof k !== "string" || k === "") {
22557 continue;
22558 }
22559 if (typeof v !== "string" || !allowed.includes(v)) {
22560 continue;
22561 }
22562 next[k] = v;
22563 }
22564 osSettings.state.itemVisibility = next;
22565 }
22566 if (Array.isArray(patch.dockOrder)) {
22567 osSettings.state.dockOrder = patch.dockOrder.filter(
22568 (v) => typeof v === "string" && v !== ""
22569 ).slice(0, 256);
22570 }
22571 if (patch.dockPromotedPositions && typeof patch.dockPromotedPositions === "object") {
22572 const MAX_COORD = 1e5;
22573 const next = {};
22574 for (const [k, v] of Object.entries(
22575 patch.dockPromotedPositions
22576 )) {
22577 if (typeof k !== "string" || k === "") {
22578 continue;
22579 }
22580 if (!v || typeof v !== "object") {
22581 continue;
22582 }
22583 const pos = v;
22584 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) {
22585 continue;
22586 }
22587 next[k] = { x: pos.x, y: pos.y };
22588 if (Object.keys(next).length >= 256) {
22589 break;
22590 }
22591 }
22592 osSettings.state.dockPromotedPositions = next;
22593 }
22594 osSettings.save(opts);
22595 if (patch.itemVisibility || patch.dockOrder) {
22596 layoutDispatcher?.refresh();
22597 }
22598 },
22599 deriveWindowId: (url, overrideAdminUrl) => deriveWindowId(url, overrideAdminUrl ?? config.adminUrl),
22600 listSystemTiles: () => layoutDispatcher?.listSystemTiles() ?? [],
22601 getSystemTile: (id) => layoutDispatcher?.getSystemTile(id) ?? null,
22602 getMenuItems: () => layoutDispatcher?.getMenuItems() ?? [],
22603 renderIcon,
22604 applyTileClasses,
22605 applyTileElement,
22606 applyTileTooltip,
22607 dispatchTileRendered,
22608 isDockElement,
22609 registerDockSelector,
22610 registerTitleBarButton,
22611 unregisterTitleBarButton,
22612 listTitleBarButtons,
22613 registerUnfocusEffect,
22614 unregisterUnfocusEffect,
22615 listUnfocusEffects,
22616 relations: relationsApi,
22617 registerWindowLinkRenderer,
22618 unregisterWindowLinkRenderer,
22619 listWindowLinkRenderers,
22620 registerWindowTheme,
22621 unregisterWindowTheme,
22622 listWindowThemes,
22623 applyWindowTheme: (windowId, override) => {
22624 const win = manager2.getById(windowId);
22625 if (!win) {
22626 return;
22627 }
22628 win.setAppearanceTheme(override);
22629 },
22630 registerWindowControl,
22631 unregisterWindowControl,
22632 listWindowControls,
22633 applyWindowControls: (windowId, override) => {
22634 const win = manager2.getById(windowId);
22635 if (!win) {
22636 return;
22637 }
22638 win.setAppearanceControls(override);
22639 },
22640 registerWindowSlot,
22641 unregisterWindowSlot,
22642 listWindowSlots,
22643 applyWindowSlot: (windowId, slot, slotConfig) => {
22644 const win = manager2.getById(windowId);
22645 if (!win) {
22646 return;
22647 }
22648 win.setAppearanceSlot(slot, slotConfig);
22649 },
22650 registerWindowNotice,
22651 unregisterWindowNotice,
22652 listWindowNotices,
22653 dismissWindowNotice,
22654 undismissWindowNotice,
22655 registerWindowChrome,
22656 unregisterWindowChrome,
22657 listWindowChromes,
22658 applyWindowChrome: (windowId, chromeId) => {
22659 const win = manager2.getById(windowId);
22660 if (!win) {
22661 return;
22662 }
22663 win.setAppearanceChrome(chromeId);
22664 },
22665 connect,
22666 getConnection,
22667 broadcast,
22668 subscribe: subscribe$2,
22669 registerPalette,
22670 unregisterPalette,
22671 listPalettes,
22672 openPalette: openPaletteOnly,
22673 devtools,
22674 createSharedStore,
22675 presence: presenceApi,
22676 activity,
22677 heartbeat,
22678 showToast,
22679 notify: notify$d,
22680 pwa: {
22681 promptInstall,
22682 undismissInstallHint,
22683 getState: getPwaState,
22684 subscribe: subscribePwaState,
22685 requestNotificationPermission,
22686 getNotificationPermission
22687 },
22688 renderKeyedList,
22689 clearKeyedList,
22690 registerNamespace: (name, api) => {
22691 if (typeof name !== "string" || name === "") {
22692 console.warn(
22693 "[desktop-mode] registerNamespace: name must be a non-empty string"
22694 );
22695 return;
22696 }
22697 if (!api || typeof api !== "object") {
22698 console.warn(
22699 `[desktop-mode] registerNamespace("${name}"): api must be an object`
22700 );
22701 return;
22702 }
22703 if (RESERVED_NAMESPACE_KEYS.has(name)) {
22704 console.warn(
22705 `[desktop-mode] registerNamespace("${name}"): name is reserved by the shell — pick a plugin-specific key`
22706 );
22707 return;
22708 }
22709 desktopApi[name] = api;
22710 },
22711 getWindowConfig: (id) => {
22712 const store2 = window.desktopModeWindowConfig;
22713 if (!store2 || typeof store2 !== "object") {
22714 return void 0;
22715 }
22716 const value = store2[id];
22717 return value === void 0 ? void 0 : value;
22718 },
22719 debug: {
22720 window: (id) => {
22721 const entry = (config.nativeWindows ?? []).find(
22722 (e) => e.id === id
22723 );
22724 if (!entry) {
22725 return null;
22726 }
22727 const url = entry.scriptUrl || "";
22728 let loadPath = "unknown";
22729 let tagInDom = false;
22730 if (url) {
22731 const lazyTag = document.querySelector(
22732 `script[data-desktop-mode-vendor="${url.replace(/"/g, '\\"')}"]`
22733 );
22734 if (lazyTag) {
22735 loadPath = "lazy";
22736 tagInDom = true;
22737 } else {
22738 const eagerTag = Array.from(
22739 document.querySelectorAll(
22740 "script[src]"
22741 )
22742 ).find((s) => s.src === url);
22743 if (eagerTag) {
22744 loadPath = "eager";
22745 tagInDom = true;
22746 }
22747 }
22748 }
22749 const cfgStore = window.desktopModeWindowConfig;
22750 const configPresent = !!(cfgStore && typeof cfgStore === "object" && Object.prototype.hasOwnProperty.call(cfgStore, id));
22751 return {
22752 id,
22753 scriptHandle: entry.scriptHandle || "",
22754 scriptUrl: url,
22755 loadPath,
22756 tagInDom,
22757 configPresent,
22758 extras: {
22759 hasTranslations: !!entry.scriptTranslations,
22760 l10nCount: (entry.scriptL10n ?? []).length,
22761 beforeCount: (entry.scriptBefore ?? []).length,
22762 afterCount: (entry.scriptAfter ?? []).length
22763 }
22764 };
22765 }
22766 }
22767 };
22768 return desktopApi;
22769 }
22770 function installPublicApi(api) {
22771 if (!window.wp) {
22772 window.wp = {};
22773 }
22774 if (!window.wp.desktop) {
22775 window.wp.desktop = api;
22776 return;
22777 }
22778 Object.assign(
22779 window.wp.desktop,
22780 api
22781 );
22782 }
22783 const store$1 = createSharedStore("desktop-mode/layout", () => ({
22784 // Default mirrors the OsSettingsSnapshot default; the shell
22785 // re-publishes the persisted value as soon as it boots.
22786 layout: "classic"
22787 }));
22788 function setCurrentLayout(layout) {
22789 if (store$1.state.layout === layout) {
22790 return;
22791 }
22792 store$1.state.layout = layout;
22793 store$1.notify();
22794 }
22795 class DesktopFile {
22796 constructor(shape) {
22797 this.shape = shape;
22798 }
22799 /** Title shown under the tile. Defaults to `shape.title`. */
22800 title() {
22801 return this.shape.title;
22802 }
22803 /** Dashicon class or data URI. Defaults to `shape.icon`. */
22804 icon() {
22805 return this.shape.icon;
22806 }
22807 /** Optional preview-image URL. Defaults to `shape.previewUrl`. */
22808 previewUrl() {
22809 return this.shape.previewUrl;
22810 }
22811 /** Reference (id, URL, …). */
22812 ref() {
22813 return this.shape.ref;
22814 }
22815 /** Whether the underlying entity still exists. */
22816 exists() {
22817 return this.shape.exists;
22818 }
22819 }
22820 class DefaultDesktopFile extends DesktopFile {
22821 constructor(shape, typeSlug) {
22822 super(shape);
22823 this.typeSlug = typeSlug;
22824 }
22825 type() {
22826 return this.typeSlug;
22827 }
22828 }
22829 const seed$1 = /* @__PURE__ */ new Map();
22830 const listeners$1 = /* @__PURE__ */ new Set();
22831 function registerType(def) {
22832 if (!def.type) {
22833 throw new Error("[desktop-mode] registerType: `type` is required.");
22834 }
22835 if (!def.label) {
22836 throw new Error("[desktop-mode] registerType: `label` is required.");
22837 }
22838 seed$1.set(def.type, {
22839 type: def.type,
22840 label: def.label,
22841 sort: typeof def.sort === "number" ? def.sort : 100,
22842 DesktopFile: def.DesktopFile
22843 });
22844 doAction("desktop-mode.files.type-registered", def.type, def);
22845 notify$1();
22846 }
22847 function unregisterType(typeSlug) {
22848 if (seed$1.delete(typeSlug)) {
22849 doAction("desktop-mode.files.type-unregistered", typeSlug);
22850 notify$1();
22851 }
22852 }
22853 function getType(typeSlug) {
22854 const entry = seed$1.get(typeSlug);
22855 return entry ? entry : null;
22856 }
22857 function getTypes() {
22858 const list2 = Array.from(seed$1.values()).slice();
22859 const filtered = applyFilters(
22860 "desktop-mode.files.types",
22861 list2
22862 );
22863 const arr = Array.isArray(filtered) ? filtered : list2;
22864 arr.sort((a, b) => {
22865 if (a.sort !== b.sort) {
22866 return a.sort - b.sort;
22867 }
22868 return a.label.localeCompare(b.label);
22869 });
22870 return arr;
22871 }
22872 function resolve(shape) {
22873 const entry = seed$1.get(shape.type);
22874 if (entry?.DesktopFile) {
22875 return new entry.DesktopFile(shape);
22876 }
22877 return new DefaultDesktopFile(shape, shape.type);
22878 }
22879 function subscribe(cb) {
22880 listeners$1.add(cb);
22881 return () => listeners$1.delete(cb);
22882 }
22883 function notify$1() {
22884 for (const cb of listeners$1) {
22885 try {
22886 cb();
22887 } catch (err) {
22888 console.error("[desktop-mode] files registry subscriber threw:", err);
22889 }
22890 }
22891 }
22892 const seed = /* @__PURE__ */ new Map();
22893 const listeners = /* @__PURE__ */ new Set();
22894 let userAssociations = {};
22895 function setUserAssociations(map) {
22896 userAssociations = { ...map };
22897 notify();
22898 }
22899 function getUserAssociations() {
22900 return { ...userAssociations };
22901 }
22902 function registerOpener(def) {
22903 if (!def.id) {
22904 throw new Error("[desktop-mode] registerOpener: `id` is required.");
22905 }
22906 if (!def.label) {
22907 throw new Error("[desktop-mode] registerOpener: `label` is required.");
22908 }
22909 if (!Array.isArray(def.types) || def.types.length === 0) {
22910 throw new Error("[desktop-mode] registerOpener: `types` must be a non-empty array.");
22911 }
22912 if (!def.handler || typeof def.handler !== "object") {
22913 throw new Error("[desktop-mode] registerOpener: `handler` is required.");
22914 }
22915 seed.set(def.id, {
22916 id: def.id,
22917 label: def.label,
22918 types: def.types.slice(),
22919 isDefault: !!def.isDefault,
22920 sort: typeof def.sort === "number" ? def.sort : 100,
22921 handler: def.handler
22922 });
22923 doAction("desktop-mode.files.opener-registered", def.id, def);
22924 notify();
22925 }
22926 function unregisterOpener(id) {
22927 if (seed.delete(id)) {
22928 doAction("desktop-mode.files.opener-unregistered", id);
22929 notify();
22930 }
22931 }
22932 function getOpener(id) {
22933 return seed.get(id) ?? null;
22934 }
22935 function getOpeners() {
22936 const list2 = Array.from(seed.values()).slice();
22937 const filtered = applyFilters(
22938 "desktop-mode.files.openers",
22939 list2
22940 );
22941 const arr = Array.isArray(filtered) ? filtered : list2;
22942 arr.sort((a, b) => {
22943 const sa = typeof a.sort === "number" ? a.sort : 100;
22944 const sb = typeof b.sort === "number" ? b.sort : 100;
22945 if (sa !== sb) {
22946 return sa - sb;
22947 }
22948 return a.label.localeCompare(b.label);
22949 });
22950 return arr;
22951 }
22952 function getOpenersForType(type) {
22953 return getOpeners().filter((e) => e.types.includes(type));
22954 }
22955 function resolveOpener(type) {
22956 const candidates = getOpenersForType(type);
22957 if (candidates.length === 0) {
22958 return null;
22959 }
22960 const override = userAssociations[type];
22961 let resolved = null;
22962 if (override) {
22963 resolved = candidates.find((e) => e.id === override) ?? null;
22964 }
22965 if (!resolved) {
22966 resolved = candidates.find((e) => e.isDefault) ?? null;
22967 }
22968 if (!resolved) {
22969 resolved = candidates[0];
22970 }
22971 const filtered = applyFilters(
22972 "desktop-mode.files.resolve-opener",
22973 resolved,
22974 type
22975 );
22976 return filtered ?? null;
22977 }
22978 function subscribeOpeners(cb) {
22979 listeners.add(cb);
22980 return () => listeners.delete(cb);
22981 }
22982 function notify() {
22983 for (const cb of listeners) {
22984 try {
22985 cb();
22986 } catch (err) {
22987 console.error("[desktop-mode] openers subscriber threw:", err);
22988 }
22989 }
22990 }
22991 let deps$2 = null;
22992 function installOpenDeps(next) {
22993 deps$2 = next;
22994 }
22995 async function openFile(file, ctx) {
22996 if (!deps$2) {
22997 console.warn(
22998 "[desktop-mode] wp.desktop.files.open() called before the shell installed open deps. The file will not open."
22999 );
23000 return false;
23001 }
23002 const opener = resolveOpener(file.type());
23003 if (!opener) {
23004 doAction("desktop-mode.files.open-failed", {
23005 reason: "no-opener",
23006 type: file.type(),
23007 ref: file.ref()
23008 });
23009 return false;
23010 }
23011 doAction("desktop-mode.files.opening", { file, openerId: opener.id });
23012 try {
23013 const handler = opener.handler;
23014 if (handler.kind === "url") {
23015 const url = await handler.url(file);
23016 if (!url) {
23017 return false;
23018 }
23019 const id = handler.windowId ? handler.windowId(file) : deps$2.deriveWindowId(url);
23020 const title = handler.title ? handler.title(file) : file.title();
23021 const icon = file.icon();
23022 const opened = deps$2.openUrl({ id, url, title, icon });
23023 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "url" });
23024 return opened;
23025 }
23026 if (handler.kind === "window") {
23027 const config = handler.config ? handler.config(file) : void 0;
23028 const opened = deps$2.openNativeWindow(handler.windowId, config);
23029 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "window" });
23030 return opened;
23031 }
23032 await handler.open(file, ctx);
23033 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "js" });
23034 return true;
23035 } catch (err) {
23036 doAction("desktop-mode.files.open-failed", {
23037 reason: "handler-threw",
23038 type: file.type(),
23039 ref: file.ref(),
23040 openerId: opener.id,
23041 error: err
23042 });
23043 console.error("[desktop-mode] file opener threw:", err);
23044 return false;
23045 }
23046 }
23047 function registerBuiltInFileTypes() {
23048 registerType({ type: "shortcut", label: "Plugin shortcut", sort: 1 });
23049 registerType({ type: "folder", label: "Folder", sort: 5 });
23050 registerType({ type: "post", label: "Post", sort: 10 });
23051 registerType({ type: "attachment", label: "Media", sort: 20 });
23052 registerType({ type: "upload", label: "Uploaded file", sort: 25 });
23053 registerType({ type: "user", label: "User", sort: 30 });
23054 registerType({ type: "term", label: "Taxonomy term", sort: 40 });
23055 registerType({ type: "comment", label: "Comment", sort: 50 });
23056 registerType({ type: "bookmark", label: "Bookmark", sort: 60 });
23057 registerType({ type: "link", label: "Web link", sort: 70 });
23058 registerType({ type: "embed", label: "Embedded web window", sort: 80 });
23059 }
23060 let deps$1 = null;
23061 function installRestDeps(next) {
23062 deps$1 = next;
23063 }
23064 function ensureDeps$1() {
23065 if (!deps$1) {
23066 throw new Error("[desktop-mode] files REST client called before installRestDeps().");
23067 }
23068 return deps$1;
23069 }
23070 function getFilesRestDeps() {
23071 return ensureDeps$1();
23072 }
23073 class FilesConflictError extends Error {
23074 constructor(detail) {
23075 super(
23076 `Row was changed by ${detail.actor.name || "another session"} (parent="${detail.current.parentName}")`
23077 );
23078 this.name = "FilesConflictError";
23079 this.status = 409;
23080 this.detail = detail;
23081 }
23082 }
23083 async function call$1(path, init2) {
23084 const { baseUrl, nonce } = ensureDeps$1();
23085 const url = joinRestUrl(baseUrl, path);
23086 const headers = new Headers(init2.headers ?? {});
23087 headers.set("X-WP-Nonce", nonce);
23088 if (init2.body && !headers.has("Content-Type")) {
23089 headers.set("Content-Type", "application/json");
23090 }
23091 const res = await trackedFetch$1(
23092 url,
23093 { ...init2, headers, credentials: "same-origin" },
23094 { source: "desktop-mode/files" }
23095 );
23096 const text = await res.text();
23097 let body = null;
23098 let parseError = null;
23099 if (text) {
23100 try {
23101 body = JSON.parse(text);
23102 } catch (e) {
23103 body = null;
23104 parseError = e;
23105 }
23106 }
23107 if (!res.ok) {
23108 if (res.status === 409) {
23109 const data = body?.data?.data ?? body?.data;
23110 if (data && typeof data === "object") {
23111 throw new FilesConflictError(data);
23112 }
23113 }
23114 const err = body;
23115 throw new Error(
23116 `[desktop-mode] files REST ${res.status}: ${err?.code ?? ""} ${err?.message ?? ""}`.trim()
23117 );
23118 }
23119 if (null === body) {
23120 if (parseError && text) {
23121 const head = text.slice(0, 120).replace(/\s+/g, " ");
23122 throw new Error(
23123 `[desktop-mode] files REST ${res.status} returned non-JSON body — ${parseError.message}. First 120 chars: ${head}`
23124 );
23125 }
23126 throw new Error(
23127 `[desktop-mode] files REST ${res.status}: empty or unparseable body.`
23128 );
23129 }
23130 return body;
23131 }
23132 function listPlacements(folderId = 0) {
23133 return call$1(
23134 `/placements?folder=${encodeURIComponent(String(folderId))}`,
23135 { method: "GET" }
23136 );
23137 }
23138 function createPlacement(body) {
23139 return call$1("/placements", {
23140 method: "POST",
23141 body: JSON.stringify(body)
23142 });
23143 }
23144 function updatePlacement(id, body, ifMatchMs) {
23145 const headers = {};
23146 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
23147 headers["If-Match"] = String(ifMatchMs);
23148 }
23149 return call$1(`/placements/${id}`, {
23150 method: "PATCH",
23151 body: JSON.stringify(body),
23152 headers
23153 });
23154 }
23155 function deletePlacement(id) {
23156 return call$1(`/placements/${id}`, { method: "DELETE" });
23157 }
23158 async function restoreTrashedItem(id, type) {
23159 const { baseUrl, nonce } = ensureDeps$1();
23160 const root = baseUrl.replace(/\/files\/?$/, "");
23161 const url = `${root}/recycle-bin/restore`;
23162 const res = await trackedFetch$1(
23163 url,
23164 {
23165 method: "POST",
23166 headers: {
23167 "Content-Type": "application/json",
23168 "X-WP-Nonce": nonce
23169 },
23170 credentials: "same-origin",
23171 body: JSON.stringify({ items: [{ id, type }] })
23172 },
23173 { source: "desktop-mode/files" }
23174 );
23175 if (!res.ok) {
23176 throw new Error(`[desktop-mode] restore ${res.status}`);
23177 }
23178 return await res.json();
23179 }
23180 function listFolders() {
23181 return call$1("/folders", { method: "GET" });
23182 }
23183 function createFolder(body) {
23184 return call$1("/folders", {
23185 method: "POST",
23186 body: JSON.stringify(body)
23187 });
23188 }
23189 function updateFolder(id, body, ifMatchMs) {
23190 const headers = {};
23191 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
23192 headers["If-Match"] = String(ifMatchMs);
23193 }
23194 return call$1(`/folders/${id}`, {
23195 method: "PATCH",
23196 body: JSON.stringify(body),
23197 headers
23198 });
23199 }
23200 function deleteFolder(id) {
23201 return call$1(`/folders/${id}`, { method: "DELETE" });
23202 }
23203 function saveAssociations(associations) {
23204 return call$1("/associations", {
23205 method: "PUT",
23206 body: JSON.stringify({ associations })
23207 });
23208 }
23209 function listShares(folderId) {
23210 return call$1(`/folders/${folderId}/shares`, { method: "GET" });
23211 }
23212 function inviteShare(folderId, body) {
23213 return call$1(`/folders/${folderId}/shares`, {
23214 method: "POST",
23215 body: JSON.stringify(body)
23216 });
23217 }
23218 function updateShareCapability(folderId, shareId, capability) {
23219 return call$1(`/folders/${folderId}/shares/${shareId}`, {
23220 method: "PATCH",
23221 body: JSON.stringify({ capability })
23222 });
23223 }
23224 function revokeShare(folderId, shareId) {
23225 return call$1(`/folders/${folderId}/shares/${shareId}`, {
23226 method: "DELETE"
23227 });
23228 }
23229 function acceptShare(folderId, shareId) {
23230 return call$1(`/folders/${folderId}/shares/${shareId}/accept`, {
23231 method: "POST"
23232 });
23233 }
23234 function denyShare(folderId, shareId) {
23235 return call$1(`/folders/${folderId}/shares/${shareId}/deny`, {
23236 method: "POST"
23237 });
23238 }
23239 function leaveShare(folderId) {
23240 return call$1(`/folders/${folderId}/leave`, {
23241 method: "POST"
23242 });
23243 }
23244 function purgeFolderSharingTables() {
23245 return call$1(
23246 "/folder-sharing-tables/purge",
23247 { method: "POST" }
23248 );
23249 }
23250 function listFileShares(fileId) {
23251 return call$1(
23252 `/uploads/${fileId}/shares`,
23253 { method: "GET" }
23254 );
23255 }
23256 function inviteFileShare(fileId, userId) {
23257 return call$1(`/uploads/${fileId}/shares`, {
23258 method: "POST",
23259 body: JSON.stringify({ userId })
23260 });
23261 }
23262 function revokeFileShare(fileId, shareId) {
23263 return call$1(
23264 `/uploads/${fileId}/shares/${shareId}`,
23265 { method: "DELETE" }
23266 );
23267 }
23268 function acceptFileShare(fileId, shareId) {
23269 return call$1(
23270 `/uploads/${fileId}/shares/${shareId}/accept`,
23271 { method: "POST" }
23272 );
23273 }
23274 function denyFileShare(fileId, shareId) {
23275 return call$1(
23276 `/uploads/${fileId}/shares/${shareId}/deny`,
23277 { method: "POST" }
23278 );
23279 }
23280 function leaveFileShare(fileId) {
23281 return call$1(`/uploads/${fileId}/leave`, {
23282 method: "POST"
23283 });
23284 }
23285 function renameUpload(fileId, name) {
23286 return call$1(
23287 `/uploads/${fileId}`,
23288 { method: "PATCH", body: JSON.stringify({ name }) }
23289 );
23290 }
23291 function ensureUploadPath(parentId, relativePath) {
23292 return call$1("/uploads/paths", {
23293 method: "POST",
23294 body: JSON.stringify({ parentId, relativePath })
23295 });
23296 }
23297 function getUploadDownloadUrl(fileId) {
23298 const { baseUrl, nonce } = ensureDeps$1();
23299 const base = joinRestUrl(baseUrl, `/uploads/${fileId}/download`);
23300 return `${base}${base.includes("?") ? "&" : "?"}_wpnonce=${encodeURIComponent(nonce)}`;
23301 }
23302 function getFolderZipUrl(folderId) {
23303 const { baseUrl, nonce } = ensureDeps$1();
23304 const base = joinRestUrl(baseUrl, `/folders/${folderId}/download`);
23305 return `${base}${base.includes("?") ? "&" : "?"}_wpnonce=${encodeURIComponent(nonce)}`;
23306 }
23307 const filesRest = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
23308 __proto__: null,
23309 FilesConflictError,
23310 acceptFileShare,
23311 acceptShare,
23312 createFolder,
23313 createPlacement,
23314 deleteFolder,
23315 deletePlacement,
23316 denyFileShare,
23317 denyShare,
23318 ensureUploadPath,
23319 getFilesRestDeps,
23320 getFolderZipUrl,
23321 getUploadDownloadUrl,
23322 installRestDeps,
23323 inviteFileShare,
23324 inviteShare,
23325 leaveFileShare,
23326 leaveShare,
23327 listFileShares,
23328 listFolders,
23329 listPlacements,
23330 listShares,
23331 purgeFolderSharingTables,
23332 renameUpload,
23333 restoreTrashedItem,
23334 revokeFileShare,
23335 revokeShare,
23336 saveAssociations,
23337 updateFolder,
23338 updatePlacement,
23339 updateShareCapability
23340 }, Symbol.toStringTag, { value: "Module" }));
23341 const STORE_KEY = "desktop-mode/files";
23342 function getFilesStore() {
23343 return createSharedStore(STORE_KEY, () => ({
23344 placementsByFolder: /* @__PURE__ */ new Map(),
23345 folders: /* @__PURE__ */ new Map(),
23346 hydratedFolders: /* @__PURE__ */ new Set()
23347 }));
23348 }
23349 function fireChanged(detail) {
23350 if (typeof document === "undefined") {
23351 return;
23352 }
23353 document.dispatchEvent(
23354 new CustomEvent("desktop-mode-files-changed", {
23355 detail: { source: "local", ...detail }
23356 })
23357 );
23358 }
23359 function setFolderPlacements(folderId, placements) {
23360 const store2 = getFilesStore();
23361 const next = new Map(store2.state.placementsByFolder);
23362 next.set(folderId, placements.slice());
23363 const hydrated = new Set(store2.state.hydratedFolders);
23364 hydrated.add(folderId);
23365 store2.state = { ...store2.state, placementsByFolder: next, hydratedFolders: hydrated };
23366 store2.notify();
23367 fireChanged({ kind: "placements-set", folderId });
23368 }
23369 function upsertPlacement(placement, source = "local") {
23370 if (!placement || typeof placement.id !== "number") {
23371 console.warn(
23372 "[desktop-mode] upsertPlacement called with a non-placement value; ignoring.",
23373 placement
23374 );
23375 return;
23376 }
23377 const store2 = getFilesStore();
23378 const next = new Map(store2.state.placementsByFolder);
23379 for (const [folderId, list2] of next) {
23380 const idx2 = list2.findIndex((p) => p && p.id === placement.id);
23381 if (idx2 >= 0 && folderId !== placement.parentId) {
23382 const copy = list2.filter(Boolean);
23383 const removeAt = copy.findIndex((p) => p.id === placement.id);
23384 if (removeAt >= 0) {
23385 copy.splice(removeAt, 1);
23386 }
23387 next.set(folderId, copy);
23388 }
23389 }
23390 const rawTarget = next.get(placement.parentId)?.slice() ?? [];
23391 const target2 = rawTarget.filter(Boolean);
23392 const idx = target2.findIndex((p) => p.id === placement.id);
23393 if (idx >= 0) {
23394 target2[idx] = placement;
23395 } else {
23396 target2.push(placement);
23397 }
23398 next.set(placement.parentId, target2);
23399 store2.state = { ...store2.state, placementsByFolder: next };
23400 store2.notify();
23401 fireChanged({ kind: "placement-upserted", placementId: placement.id, folderId: placement.parentId, source });
23402 }
23403 function removePlacement(placementId, source = "local") {
23404 const store2 = getFilesStore();
23405 const next = new Map(store2.state.placementsByFolder);
23406 let touchedFolder;
23407 for (const [folderId, list2] of next) {
23408 const idx = list2.findIndex((p) => p && p.id === placementId);
23409 if (idx >= 0) {
23410 const copy = list2.filter(Boolean).filter(
23411 (p) => p.id !== placementId
23412 );
23413 next.set(folderId, copy);
23414 touchedFolder = folderId;
23415 }
23416 }
23417 if (touchedFolder === void 0) {
23418 return;
23419 }
23420 store2.state = { ...store2.state, placementsByFolder: next };
23421 store2.notify();
23422 fireChanged({ kind: "placement-removed", placementId, folderId: touchedFolder, source });
23423 }
23424 function setFolders(folders) {
23425 const store2 = getFilesStore();
23426 const next = /* @__PURE__ */ new Map();
23427 for (const f of folders) {
23428 next.set(f.id, f);
23429 }
23430 store2.state = { ...store2.state, folders: next };
23431 store2.notify();
23432 fireChanged({ kind: "folders-set" });
23433 }
23434 function upsertFolder(folder, source = "local") {
23435 const store2 = getFilesStore();
23436 const next = new Map(store2.state.folders);
23437 next.set(folder.id, folder);
23438 store2.state = { ...store2.state, folders: next };
23439 store2.notify();
23440 fireChanged({ kind: "folder-upserted", folderRowId: folder.id, source });
23441 }
23442 function removeFolder(folderId, source = "local") {
23443 const store2 = getFilesStore();
23444 const folders = new Map(store2.state.folders);
23445 folders.delete(folderId);
23446 const placements = new Map(store2.state.placementsByFolder);
23447 placements.delete(folderId);
23448 store2.state = { ...store2.state, folders, placementsByFolder: placements };
23449 store2.notify();
23450 fireChanged({ kind: "folder-removed", folderRowId: folderId, source });
23451 }
23452 function subscribeFilesStore(cb) {
23453 const store2 = getFilesStore();
23454 const off = store2.subscribe(cb);
23455 return off;
23456 }
23457 function getFilesState() {
23458 return getFilesStore().getState();
23459 }
23460 function currentPlacement(snapshot) {
23461 const state2 = getFilesState();
23462 const sameFolder = state2.placementsByFolder.get(snapshot.parentId)?.find((p) => p && p.id === snapshot.id);
23463 if (sameFolder) {
23464 return sameFolder;
23465 }
23466 for (const list2 of state2.placementsByFolder.values()) {
23467 const hit = list2.find((p) => p && p.id === snapshot.id);
23468 if (hit) {
23469 return hit;
23470 }
23471 }
23472 return snapshot;
23473 }
23474 const store = {
23475 getState: getFilesState,
23476 subscribe: subscribeFilesStore,
23477 setFolderPlacements,
23478 upsertPlacement,
23479 upsertFolder,
23480 removePlacement,
23481 removeFolder,
23482 currentPlacement
23483 };
23484 const styles$4 = css`:host{display:inline-block}`;
23485 const styles$3 = css`:host{position:absolute;width:var( --wpd-ribbon-size,90px );height:var( --wpd-ribbon-size,90px );overflow:hidden;pointer-events:none;z-index:var( --wpd-ribbon-z,2 )}:host( [ hidden ] ){display:none}.banner{position:absolute;display:block;width:var( --wpd-ribbon-banner-width,140px );padding:var( --wpd-ribbon-padding,4px 0 );text-align:center;font:var( --wpd-ribbon-font,700 10px/1.4 var( --desktop-mode-font,system-ui ) );letter-spacing:var( --wpd-ribbon-tracking,0.06em );text-transform:uppercase;color:var( --wpd-ribbon-fg,#fff );background:var( --wpd-ribbon-bg,var( --wp-admin-theme-color,#2271b1 ) );box-shadow:var( --wpd-ribbon-shadow,0 2px 4px rgba( 0,0,0,0.2 ) )}:host(:not( [ placement ] ) ),:host( [ placement='top-end' ] ){inset-block-start:0;inset-inline-end:0}:host(:not( [ placement ] ) ) .banner,:host( [ placement='top-end' ] ) .banner{inset-block-start:var( --wpd-ribbon-banner-offset,20px );inset-inline-end:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( 45deg )}:host( [ placement='top-start' ] ){inset-block-start:0;inset-inline-start:0}:host( [ placement='top-start' ] ) .banner{inset-block-start:var( --wpd-ribbon-banner-offset,20px );inset-inline-start:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( -45deg )}:host( [ placement='bottom-end' ] ){inset-block-end:0;inset-inline-end:0}:host( [ placement='bottom-end' ] ) .banner{inset-block-end:var( --wpd-ribbon-banner-offset,20px );inset-inline-end:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( -45deg )}:host( [ placement='bottom-start' ] ){inset-block-end:0;inset-inline-start:0}:host( [ placement='bottom-start' ] ) .banner{inset-block-end:var( --wpd-ribbon-banner-offset,20px );inset-inline-start:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( 45deg )}:host-context( [ dir='rtl' ] ):host(:not( [ placement ] ) ) .banner,:host-context( [ dir='rtl' ] ):host( [ placement='top-end' ] ) .banner{transform:rotate( -45deg )}:host-context( [ dir='rtl' ] ):host( [ placement='top-start' ] ) .banner{transform:rotate( 45deg )}:host-context( [ dir='rtl' ] ):host( [ placement='bottom-end' ] ) .banner{transform:rotate( 45deg )}:host-context( [ dir='rtl' ] ):host( [ placement='bottom-start' ] ) .banner{transform:rotate( -45deg )}:host( [ tone='success' ] ) .banner{background:var( --wpd-ribbon-success,#1a7f37 )}:host( [ tone='warning' ] ) .banner{background:var( --wpd-ribbon-warning,#9a6700 )}:host( [ tone='danger' ] ) .banner{background:var( --wpd-ribbon-danger,#cf222e )}:host( [ tone='info' ] ) .banner{background:var( --wpd-ribbon-info,#0969da )}:host( [ tone='neutral' ] ) .banner{background:var( --wpd-ribbon-neutral,#57606a )}`;
23486 const _WpdRibbon = class _WpdRibbon extends Component {
23487 render() {
23488 return html`<span class="banner" part="banner"><slot></slot></span>`;
23489 }
23490 };
23491 _WpdRibbon.props = ["placement", "tone"];
23492 _WpdRibbon.styles = [styles$3];
23493 _WpdRibbon.help = {
23494 title: "Ribbon",
23495 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.",
23496 status: "experimental",
23497 since: "0.8.6",
23498 props: [
23499 {
23500 name: "placement",
23501 type: '"top-end" | "top-start" | "bottom-end" | "bottom-start"',
23502 description: "Which corner of the parent the ribbon hugs. Defaults to `top-end` (logical right in LTR, left in RTL)."
23503 },
23504 {
23505 name: "tone",
23506 type: '"primary" | "success" | "warning" | "danger" | "info" | "neutral"',
23507 description: "Background color tone. Defaults to `primary` (the admin theme accent)."
23508 }
23509 ],
23510 slots: [{ name: "(default)", description: "Ribbon label text. Keep short." }],
23511 cssProps: [
23512 { name: "--wpd-ribbon-size", default: "90px", description: "Square clipping window edge." },
23513 { name: "--wpd-ribbon-banner-width", default: "140px", description: "Width of the rotated strip." },
23514 { name: "--wpd-ribbon-banner-offset", default: "20px", description: "Distance from corner to strip center." },
23515 { name: "--wpd-ribbon-banner-pull", default: "-36px", description: "How far the strip overhangs the clip edge." },
23516 { name: "--wpd-ribbon-bg", default: "var(--wp-admin-theme-color, #2271b1)" },
23517 { name: "--wpd-ribbon-fg", default: "#fff" },
23518 { name: "--wpd-ribbon-shadow", default: "0 2px 4px rgba(0,0,0,0.2)" },
23519 { name: "--wpd-ribbon-padding", default: "4px 0" },
23520 { name: "--wpd-ribbon-font", default: "700 10px/1.4 system-ui" },
23521 { name: "--wpd-ribbon-tracking", default: "0.06em" },
23522 { name: "--wpd-ribbon-z", default: "2" }
23523 ],
23524 example: html`
23525 <div
23526 style="position: relative; width: 240px; height: 120px;
23527 border: 1px solid #ccc; border-radius: 8px;
23528 padding: 16px; box-sizing: border-box;"
23529 >
23530 <wpd-ribbon>Featured</wpd-ribbon>
23531 Card body…
23532 </div>
23533 `
23534 };
23535 let WpdRibbon = _WpdRibbon;
23536 defineComponent("wpd-ribbon", WpdRibbon);
23537 const TILE_CLASS = "desktop-mode-file-tile";
23538 const STATUS_LABEL = {
23539 draft: "Draft",
23540 pending: "Pending",
23541 private: "Private",
23542 future: "Scheduled"
23543 };
23544 function statusRibbonsEnabled() {
23545 const get2 = window.wp?.desktop?.getOsSettings;
23546 if (typeof get2 !== "function") {
23547 return true;
23548 }
23549 try {
23550 return get2()?.showPostStatusRibbons !== false;
23551 } catch {
23552 return true;
23553 }
23554 }
23555 function getDragManager$3() {
23556 const api = window.wp?.desktop?.dragManager;
23557 return api ?? null;
23558 }
23559 const REACTIVE_PROPS = [
23560 "type",
23561 "ref",
23562 "label",
23563 "icon",
23564 "thumbnail",
23565 "kind",
23566 "status",
23567 "selected",
23568 "missing",
23569 "access-gated",
23570 "drag-kind",
23571 "drag-title",
23572 "drag-icon"
23573 ];
23574 const _WpdTile = class _WpdTile extends Component {
23575 constructor() {
23576 super(...arguments);
23577 this._pointerdownHandler = null;
23578 this._keydownHandler = null;
23579 }
23580 connectedCallback() {
23581 super.connectedCallback();
23582 if (!this._keydownHandler) {
23583 this._keydownHandler = (e) => {
23584 if (e.key === "Enter" || e.key === " ") {
23585 e.preventDefault();
23586 this.click();
23587 }
23588 };
23589 this.addEventListener("keydown", this._keydownHandler);
23590 }
23591 this._paint();
23592 }
23593 disconnectedCallback() {
23594 if (this._pointerdownHandler) {
23595 this.removeEventListener(
23596 "pointerdown",
23597 this._pointerdownHandler
23598 );
23599 this._pointerdownHandler = null;
23600 }
23601 if (this._keydownHandler) {
23602 this.removeEventListener(
23603 "keydown",
23604 this._keydownHandler
23605 );
23606 this._keydownHandler = null;
23607 }
23608 }
23609 /**
23610 * Bypass the templated render loop. Lit-html's `render(template,
23611 * root)` would wipe the host's light-DOM children every tick —
23612 * including the visual / label / ribbon `_paint()` just
23613 * inserted. We override `requestUpdate` directly so attribute
23614 * changes call `_paint` (idempotent) without lit-html getting
23615 * involved.
23616 */
23617 requestUpdate() {
23618 if (!this.isConnected) {
23619 return;
23620 }
23621 this._paint();
23622 }
23623 render() {
23624 return html``;
23625 }
23626 _paint() {
23627 const type = this.getAttribute("type") ?? "";
23628 const ref = this.getAttribute("ref") ?? "";
23629 const label = this.getAttribute("label") ?? "";
23630 const icon = this.getAttribute("icon") ?? "";
23631 const thumbnail = this.getAttribute("thumbnail") ?? "";
23632 const kind = this.getAttribute("kind") ?? "entry";
23633 const status = this.getAttribute("status") ?? "";
23634 const selected = this.hasAttribute("selected");
23635 const missing = this.hasAttribute("missing");
23636 const accessGated = this.hasAttribute("access-gated");
23637 const ownedClasses = [
23638 TILE_CLASS,
23639 `${TILE_CLASS}--folder`,
23640 `${TILE_CLASS}--missing`,
23641 `${TILE_CLASS}--access-gated`,
23642 `${TILE_CLASS}--selected`
23643 ];
23644 for (const c of ownedClasses) {
23645 this.classList.remove(c);
23646 }
23647 this.classList.add(TILE_CLASS);
23648 if (kind === "folder") {
23649 this.classList.add(`${TILE_CLASS}--folder`);
23650 }
23651 if (missing) {
23652 this.classList.add(`${TILE_CLASS}--missing`);
23653 }
23654 if (accessGated) {
23655 this.classList.add(`${TILE_CLASS}--access-gated`);
23656 }
23657 if (selected) {
23658 this.classList.add(`${TILE_CLASS}--selected`);
23659 }
23660 this.dataset.fileType = type;
23661 this.dataset.fileRef = ref;
23662 if (kind) {
23663 this.dataset.role = kind;
23664 }
23665 this.setAttribute("role", "listitem");
23666 this.setAttribute("aria-label", label);
23667 if (!this.hasAttribute("tabindex")) {
23668 this.setAttribute("tabindex", "0");
23669 }
23670 const accessGatedTitle = "You don’t have permission to open this — ask the folder owner for access.";
23671 if (accessGated) {
23672 this.title = accessGatedTitle;
23673 this.setAttribute("aria-disabled", "true");
23674 } else {
23675 this.removeAttribute("aria-disabled");
23676 if (this.title === accessGatedTitle) {
23677 this.removeAttribute("title");
23678 }
23679 }
23680 const SLOTS = [
23681 `${TILE_CLASS}__visual`,
23682 `${TILE_CLASS}__label`,
23683 `${TILE_CLASS}__lock`
23684 ];
23685 for (const cls of SLOTS) {
23686 this.querySelectorAll(`:scope > .${cls}`).forEach(
23687 (n) => n.remove()
23688 );
23689 }
23690 this.querySelectorAll(":scope > wpd-ribbon").forEach(
23691 (n) => n.remove()
23692 );
23693 const visual = document.createElement("span");
23694 visual.className = `${TILE_CLASS}__visual`;
23695 if (thumbnail) {
23696 const img = document.createElement("img");
23697 img.src = thumbnail;
23698 img.alt = "";
23699 img.loading = "lazy";
23700 img.decoding = "async";
23701 img.className = `${TILE_CLASS}__preview`;
23702 img.draggable = false;
23703 visual.appendChild(img);
23704 } else if (icon) {
23705 const iconNode = renderIcon(icon, {
23706 title: label,
23707 className: `${TILE_CLASS}__icon`
23708 });
23709 visual.appendChild(iconNode);
23710 }
23711 this.appendChild(visual);
23712 const labelNode = document.createElement("span");
23713 labelNode.className = `${TILE_CLASS}__label`;
23714 labelNode.textContent = label;
23715 this.appendChild(labelNode);
23716 if (accessGated) {
23717 const lock = document.createElement("span");
23718 lock.className = `${TILE_CLASS}__lock dashicons dashicons-lock`;
23719 lock.setAttribute("aria-hidden", "true");
23720 this.appendChild(lock);
23721 }
23722 if (status && status !== "publish" && STATUS_LABEL[status] && statusRibbonsEnabled()) {
23723 const ribbon = document.createElement("wpd-ribbon");
23724 ribbon.setAttribute("placement", "top-end");
23725 ribbon.setAttribute("tone", ribbonToneFor(status));
23726 ribbon.textContent = STATUS_LABEL[status];
23727 this.appendChild(ribbon);
23728 }
23729 applyTileEntryStagger(this);
23730 doAction("desktop-mode.tile.rendered", { tile: this });
23731 this._wireDragOut();
23732 }
23733 _wireDragOut() {
23734 if (this._pointerdownHandler) {
23735 this.removeEventListener(
23736 "pointerdown",
23737 this._pointerdownHandler
23738 );
23739 this._pointerdownHandler = null;
23740 }
23741 const dragKind = this.getAttribute("drag-kind");
23742 if (!dragKind) {
23743 return;
23744 }
23745 const handler = (e) => {
23746 if (e.button !== 0) {
23747 return;
23748 }
23749 const dragManager = getDragManager$3();
23750 if (!dragManager) {
23751 return;
23752 }
23753 const ref = this.getAttribute("ref") ?? "";
23754 const title = this.getAttribute("drag-title") ?? this.getAttribute("label") ?? void 0;
23755 const icon = this.getAttribute("drag-icon") ?? this.getAttribute("icon") ?? void 0;
23756 const rect = this.getBoundingClientRect();
23757 dragManager.start({
23758 payload: {
23759 type: "shortcut",
23760 source: this,
23761 data: {
23762 kind: dragKind,
23763 ref,
23764 title,
23765 icon
23766 },
23767 ghost: {
23768 offsetX: e.clientX - rect.left,
23769 offsetY: e.clientY - rect.top
23770 }
23771 },
23772 origin: e
23773 });
23774 };
23775 this._pointerdownHandler = handler;
23776 this.addEventListener("pointerdown", handler);
23777 }
23778 };
23779 _WpdTile.shadow = false;
23780 _WpdTile.props = REACTIVE_PROPS;
23781 _WpdTile.styles = [styles$4];
23782 _WpdTile.help = {
23783 title: "Tile",
23784 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.",
23785 status: "experimental",
23786 since: "0.8.6",
23787 props: [
23788 { name: "type", type: "string" },
23789 { name: "ref", type: "string" },
23790 { name: "label", type: "string" },
23791 { name: "icon", type: "string", description: "Dashicon class / URL / data URI. Ignored when `thumbnail` is set." },
23792 { name: "thumbnail", type: "string", description: "Preview image URL. Renders as `<img>` and wins over `icon`." },
23793 { name: "kind", type: "`entry` | `folder`" },
23794 { name: "status", type: "`draft` | `pending` | `private` | `future` | `publish`" },
23795 { name: "selected", type: "boolean" },
23796 { name: "missing", type: "boolean" },
23797 { name: "access-gated", type: "boolean" },
23798 { name: "drag-kind", type: "string", description: "When set, the component wires pointerdown → DragManager." },
23799 { name: "drag-title", type: "string" },
23800 { name: "drag-icon", type: "string" }
23801 ]
23802 };
23803 let WpdTile = _WpdTile;
23804 function ribbonToneFor(status) {
23805 switch (status) {
23806 case "draft":
23807 return "warning";
23808 case "pending":
23809 return "info";
23810 case "private":
23811 return "danger";
23812 case "future":
23813 return "primary";
23814 default:
23815 return "primary";
23816 }
23817 }
23818 defineComponent("wpd-tile", WpdTile);
23819 function buildTileFromSpec(spec) {
23820 const tile2 = document.createElement("wpd-tile");
23821 tile2.setAttribute("type", spec.type);
23822 tile2.setAttribute("ref", spec.ref);
23823 tile2.setAttribute("label", spec.label);
23824 if (spec.icon) {
23825 tile2.setAttribute("icon", spec.icon);
23826 }
23827 if (spec.thumbnail) {
23828 tile2.setAttribute("thumbnail", spec.thumbnail);
23829 }
23830 if (spec.role) {
23831 tile2.setAttribute("kind", spec.role);
23832 }
23833 if (spec.status) {
23834 tile2.setAttribute("status", spec.status);
23835 }
23836 if (spec.missing) {
23837 tile2.setAttribute("missing", "");
23838 }
23839 if (spec.accessGated) {
23840 tile2.setAttribute("access-gated", "");
23841 }
23842 if (spec.dataset) {
23843 for (const [key, raw] of Object.entries(spec.dataset)) {
23844 if (raw === void 0 || raw === null) {
23845 continue;
23846 }
23847 tile2.dataset[key] = String(raw);
23848 }
23849 }
23850 if (Array.isArray(spec.extraClasses)) {
23851 for (const c of spec.extraClasses) {
23852 if (c) {
23853 tile2.classList.add(c);
23854 }
23855 }
23856 }
23857 const classFiltered = applyFilters(
23858 "desktop-mode.tile.class",
23859 tile2.className,
23860 spec
23861 );
23862 if (classFiltered && classFiltered !== tile2.className) {
23863 tile2.className = classFiltered;
23864 }
23865 if (typeof spec.x === "number" && typeof spec.y === "number") {
23866 tile2.style.position = "absolute";
23867 tile2.style.left = `${spec.x}px`;
23868 tile2.style.top = `${spec.y}px`;
23869 }
23870 return tile2;
23871 }
23872 function placementLabel(placement) {
23873 const metaName = placement.meta && typeof placement.meta.name === "string" ? placement.meta.name.trim() : "";
23874 return metaName !== "" ? metaName : resolve(placement.file).title();
23875 }
23876 function placementToSpec(placement, folderId) {
23877 const file = resolve(placement.file);
23878 const previewUrl = file.previewUrl();
23879 const label = placementLabel(placement);
23880 const metaIconUrl = placement.meta && typeof placement.meta.iconUrl === "string" ? placement.meta.iconUrl.trim() : "";
23881 return {
23882 type: placement.file.type,
23883 ref: placement.file.ref,
23884 label,
23885 // Preview wins over icon (matches the previous behavior).
23886 thumbnail: previewUrl || void 0,
23887 icon: previewUrl ? void 0 : metaIconUrl || file.icon(),
23888 x: placement.x,
23889 y: placement.y,
23890 dataset: {
23891 placementId: placement.id,
23892 folderId
23893 },
23894 meta: placement.meta,
23895 missing: !placement.file.exists,
23896 accessGated: Boolean(placement.accessGated),
23897 ariaLabel: label
23898 };
23899 }
23900 function buildTile(placement, folderId) {
23901 const tile2 = buildTileFromSpec(placementToSpec(placement, folderId));
23902 const classFiltered = applyFilters(
23903 "desktop-mode.files.tile-class",
23904 TILE_CLASS,
23905 placement
23906 );
23907 if (classFiltered && classFiltered !== TILE_CLASS) {
23908 tile2.className = classFiltered;
23909 }
23910 const extra = applyFilters(
23911 "desktop-mode.files.tile-element",
23912 null,
23913 placement
23914 );
23915 if (extra instanceof Element) {
23916 tile2.appendChild(extra);
23917 }
23918 tile2.addEventListener("dblclick", (e) => {
23919 e.preventDefault();
23920 e.stopPropagation();
23921 const live = currentPlacement(placement);
23922 const file = resolve(live.file);
23923 if (live.accessGated) {
23924 showToast({
23925 message: `You don’t have permission to open "${live.file.title || file.title()}". Ask the folder owner if you need access to this item.`,
23926 duration: 6e3
23927 });
23928 return;
23929 }
23930 void openFile(file, {
23931 placement: {
23932 id: live.id,
23933 x: live.x,
23934 y: live.y,
23935 meta: live.meta
23936 }
23937 });
23938 });
23939 doAction("desktop-mode.files.tile-rendered", { tile: tile2, placement });
23940 return tile2;
23941 }
23942 function setTilePosition(tile2, x, y) {
23943 tile2.style.left = `${x}px`;
23944 tile2.style.top = `${y}px`;
23945 }
23946 const handlers$2 = /* @__PURE__ */ new Map();
23947 function registerTilePayloadHandler(type, handler) {
23948 handlers$2.set(type, handler);
23949 return () => {
23950 if (handlers$2.get(type) === handler) {
23951 handlers$2.delete(type);
23952 }
23953 };
23954 }
23955 function tilePayloadAcceptLabel(type, ctx) {
23956 const handler = handlers$2.get(type);
23957 return handler && handler.appliesTo(ctx) ? handler.acceptLabel : void 0;
23958 }
23959 function tilePayloadAccepts(payload, ctx) {
23960 const handler = handlers$2.get(payload.type);
23961 return handler ? handler.appliesTo(ctx) && handler.accept(payload.data, ctx) : false;
23962 }
23963 function tilePayloadDrop(session, ev, ctx) {
23964 const handler = handlers$2.get(session.payload.type);
23965 if (!handler || !handler.appliesTo(ctx)) {
23966 return false;
23967 }
23968 handler.onDrop(session, ev, ctx);
23969 return true;
23970 }
23971 function attachDismissable(host, options) {
23972 const onAway = (e) => {
23973 if (e.target instanceof Node && host.contains(e.target)) {
23974 return;
23975 }
23976 if (e.target instanceof Node) {
23977 for (const sel of options.siblingSelectors ?? []) {
23978 const matches = Array.from(
23979 document.querySelectorAll(sel)
23980 );
23981 for (const m of matches) {
23982 if (m.contains(e.target)) {
23983 return;
23984 }
23985 }
23986 }
23987 }
23988 if (options.excludeOutsideTarget && e.target instanceof Node && options.excludeOutsideTarget.contains(e.target)) {
23989 return;
23990 }
23991 options.close();
23992 };
23993 const onKey = (e) => {
23994 if (e.key === "Escape") {
23995 options.close();
23996 }
23997 };
23998 document.addEventListener("mousedown", onAway, { capture: true });
23999 document.addEventListener("keydown", onKey);
24000 return () => {
24001 document.removeEventListener("mousedown", onAway, { capture: true });
24002 document.removeEventListener("keydown", onKey);
24003 };
24004 }
24005 const MENU_CLASS$2 = "desktop-mode-wallpaper-menu";
24006 let activeMenu$2 = null;
24007 function closeTileMenu() {
24008 if (!activeMenu$2) {
24009 return;
24010 }
24011 activeMenu$2.dispatchEvent(new CustomEvent("tile-menu-closed"));
24012 activeMenu$2.remove();
24013 activeMenu$2 = null;
24014 doAction("desktop-mode.files.tile-menu.closed", {});
24015 }
24016 let openGeneration$1 = 0;
24017 function openTileMenu(pos, opts) {
24018 closeTileMenu();
24019 const myGen = ++openGeneration$1;
24020 openWithShellOverlays(
24021 () => myGen === openGeneration$1,
24022 () => openTileMenuImmediate(pos, opts)
24023 );
24024 }
24025 function openTileMenuImmediate(pos, { placement, items }) {
24026 const list2 = applyFilters(
24027 "desktop-mode.files.tile-menu",
24028 items.slice(),
24029 placement
24030 );
24031 const sorted = (Array.isArray(list2) ? list2 : items).slice().sort((a, b) => {
24032 const sa = typeof a.sort === "number" ? a.sort : 100;
24033 const sb = typeof b.sort === "number" ? b.sort : 100;
24034 if (sa !== sb) {
24035 return sa - sb;
24036 }
24037 return a.label.localeCompare(b.label);
24038 });
24039 if (sorted.length === 0) {
24040 return;
24041 }
24042 const menu = document.createElement("wpd-context-menu");
24043 menu.setAttribute("open", "");
24044 menu.classList.add(MENU_CLASS$2);
24045 menu.dataset.placementId = String(placement.id);
24046 menu.style.left = `${pos.x}px`;
24047 menu.style.top = `${pos.y}px`;
24048 const itemById = /* @__PURE__ */ new Map();
24049 for (const item of sorted) {
24050 itemById.set(item.id, item);
24051 const opt = document.createElement("wpd-context-menu-option");
24052 opt.dataset.menuItemId = item.id;
24053 opt.setAttribute("value", item.id);
24054 if (item.danger) {
24055 opt.setAttribute("danger", "");
24056 }
24057 if (item.disabled) {
24058 opt.setAttribute("disabled", "");
24059 }
24060 if (item.icon) {
24061 opt.setAttribute("icon", sanitizeClass$2(item.icon));
24062 }
24063 opt.textContent = item.label;
24064 menu.appendChild(opt);
24065 }
24066 menu.addEventListener("wpd-context-menu-pick", (e) => {
24067 const detail = e.detail;
24068 const item = itemById.get(detail.id);
24069 if (!item) {
24070 return;
24071 }
24072 closeTileMenu();
24073 void item.onClick(new MouseEvent("click"));
24074 });
24075 document.body.appendChild(menu);
24076 activeMenu$2 = menu;
24077 const rect = menu.getBoundingClientRect();
24078 if (rect.right > window.innerWidth) {
24079 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
24080 }
24081 if (rect.bottom > window.innerHeight) {
24082 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
24083 }
24084 const detach = attachDismissable(menu, {
24085 close: () => closeTileMenu()
24086 });
24087 menu.addEventListener("tile-menu-closed", detach);
24088 doAction("desktop-mode.files.tile-menu.opened", {
24089 placementId: placement.id,
24090 items: sorted.map((i) => i.id)
24091 });
24092 }
24093 function sanitizeClass$2(raw) {
24094 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
24095 }
24096 const ROOT_CLASS$3 = "desktop-mode-create-folder-dialog";
24097 let active$1 = null;
24098 function closeCreateFolderDialog() {
24099 if (!active$1) {
24100 return;
24101 }
24102 active$1.dispatchEvent(new CustomEvent("create-folder-dialog-closed"));
24103 active$1.remove();
24104 active$1 = null;
24105 doAction("desktop-mode.files.create-folder.closed", {});
24106 }
24107 function openCreateFolderDialog(options) {
24108 closeCreateFolderDialog();
24109 const decision = applyFilters(
24110 "desktop-mode.files.create-folder.dialog",
24111 null,
24112 options
24113 );
24114 if (decision === false) {
24115 return;
24116 }
24117 const initial = (options.initialName ?? "Untitled folder").trim();
24118 const overlay = document.createElement("div");
24119 overlay.className = `${ROOT_CLASS$3}__overlay`;
24120 overlay.setAttribute("role", "presentation");
24121 const dialog2 = document.createElement("div");
24122 dialog2.className = ROOT_CLASS$3;
24123 dialog2.setAttribute("role", "dialog");
24124 dialog2.setAttribute("aria-modal", "true");
24125 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS$3}-title`);
24126 const title = document.createElement("h2");
24127 title.id = `${ROOT_CLASS$3}-title`;
24128 title.className = `${ROOT_CLASS$3}__title`;
24129 title.textContent = options.title ?? "New folder";
24130 dialog2.appendChild(title);
24131 const label = document.createElement("label");
24132 label.className = `${ROOT_CLASS$3}__label`;
24133 label.htmlFor = `${ROOT_CLASS$3}-input`;
24134 label.textContent = options.label ?? "Folder name";
24135 dialog2.appendChild(label);
24136 const input = document.createElement("input");
24137 input.type = "text";
24138 input.id = `${ROOT_CLASS$3}-input`;
24139 input.className = `${ROOT_CLASS$3}__input`;
24140 input.value = initial;
24141 input.setAttribute("autocomplete", "off");
24142 input.setAttribute("spellcheck", "false");
24143 dialog2.appendChild(input);
24144 const error = document.createElement("p");
24145 error.className = `${ROOT_CLASS$3}__error`;
24146 error.hidden = true;
24147 error.setAttribute("role", "alert");
24148 dialog2.appendChild(error);
24149 const actions = document.createElement("div");
24150 actions.className = `${ROOT_CLASS$3}__actions`;
24151 const cancel = document.createElement("button");
24152 cancel.type = "button";
24153 cancel.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--secondary`;
24154 cancel.textContent = "Cancel";
24155 const submit = document.createElement("button");
24156 submit.type = "button";
24157 submit.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--primary`;
24158 submit.textContent = options.submitLabel ?? "Create";
24159 actions.appendChild(cancel);
24160 actions.appendChild(submit);
24161 dialog2.appendChild(actions);
24162 overlay.appendChild(dialog2);
24163 document.body.appendChild(overlay);
24164 active$1 = overlay;
24165 input.focus();
24166 input.select();
24167 doAction("desktop-mode.files.create-folder.opened", {});
24168 const setBusy = (busy) => {
24169 input.disabled = busy;
24170 cancel.disabled = busy;
24171 submit.disabled = busy;
24172 dialog2.classList.toggle(`${ROOT_CLASS$3}--busy`, busy);
24173 };
24174 const showError = (msg) => {
24175 error.textContent = msg;
24176 error.hidden = false;
24177 };
24178 const doCancel = () => {
24179 closeCreateFolderDialog();
24180 options.onCancel?.();
24181 };
24182 const doSubmit = async () => {
24183 const name = input.value.trim();
24184 if (!name) {
24185 showError("Please enter a name.");
24186 input.focus();
24187 return;
24188 }
24189 error.hidden = true;
24190 setBusy(true);
24191 try {
24192 await options.onSubmit(name);
24193 closeCreateFolderDialog();
24194 } catch (err) {
24195 setBusy(false);
24196 showError(
24197 err instanceof Error ? err.message : "Could not create the folder."
24198 );
24199 input.focus();
24200 input.select();
24201 }
24202 };
24203 cancel.addEventListener("click", () => doCancel());
24204 submit.addEventListener("click", () => void doSubmit());
24205 overlay.addEventListener("click", (e) => {
24206 if (e.target === overlay) {
24207 doCancel();
24208 }
24209 });
24210 const onKey = (e) => {
24211 if (e.key === "Escape") {
24212 e.preventDefault();
24213 doCancel();
24214 } else if (e.key === "Enter" && !e.isComposing) {
24215 e.preventDefault();
24216 void doSubmit();
24217 }
24218 };
24219 dialog2.addEventListener("keydown", onKey);
24220 overlay.addEventListener("create-folder-dialog-closed", () => {
24221 dialog2.removeEventListener("keydown", onKey);
24222 });
24223 }
24224 const GRID_PADDING = 16;
24225 const GRID_CELL_W = 96;
24226 const GRID_CELL_H = 110;
24227 function pointToCell(x, y) {
24228 const col = Math.max(0, Math.round((x - GRID_PADDING) / GRID_CELL_W));
24229 const row = Math.max(0, Math.round((y - GRID_PADDING) / GRID_CELL_H));
24230 return cellToPos(col, row);
24231 }
24232 function cellToPos(col, row) {
24233 return {
24234 col,
24235 row,
24236 x: GRID_PADDING + col * GRID_CELL_W,
24237 y: GRID_PADDING + row * GRID_CELL_H
24238 };
24239 }
24240 function snapToEmptyCell(x, y, occupied, host) {
24241 const target2 = pointToCell(x, y);
24242 if (!occupied.has(cellKey(target2.col, target2.row))) {
24243 return target2;
24244 }
24245 const maxRows = host ? Math.max(1, Math.floor((host.clientHeight - GRID_PADDING) / GRID_CELL_H)) : 999;
24246 for (let col = 0; col < 999; col++) {
24247 for (let row = 0; row < maxRows; row++) {
24248 if (!occupied.has(cellKey(col, row))) {
24249 return cellToPos(col, row);
24250 }
24251 }
24252 }
24253 return target2;
24254 }
24255 function nextRowMajorCell(occupied, host) {
24256 const cols = host ? Math.max(
24257 1,
24258 Math.floor((host.clientWidth - GRID_PADDING) / GRID_CELL_W)
24259 ) : 4;
24260 const maxCols = Math.max(1, cols);
24261 for (let row = 0; row < 999; row++) {
24262 for (let col = 0; col < maxCols; col++) {
24263 if (!occupied.has(cellKey(col, row))) {
24264 return cellToPos(col, row);
24265 }
24266 }
24267 }
24268 return cellToPos(0, 0);
24269 }
24270 function buildOccupiedSet(placements, excludeId) {
24271 const out = /* @__PURE__ */ new Set();
24272 for (const p of placements) {
24273 const cell = pointToCell(p.x, p.y);
24274 out.add(cellKey(cell.col, cell.row));
24275 }
24276 return out;
24277 }
24278 function cellKey(col, row) {
24279 return `${col},${row}`;
24280 }
24281 function isConflict(err) {
24282 return err instanceof FilesConflictError;
24283 }
24284 function buildReason(err) {
24285 const actor = err.detail.actor.name || "Someone else";
24286 const where = err.detail.current.parentName || "another folder";
24287 if (err.detail.reason === "trashed") {
24288 return "This item is in the recycle bin.";
24289 }
24290 if (err.detail.reason === "forbidden") {
24291 return "You no longer have access.";
24292 }
24293 if (err.detail.reason === "gone") {
24294 return "This item was deleted.";
24295 }
24296 return `${actor} moved this to "${where}".`;
24297 }
24298 function showConflictToast(err) {
24299 const reason = buildReason(err);
24300 const targetParentId = err.detail.current.parentId;
24301 let action;
24302 if (targetParentId > 0) {
24303 action = {
24304 label: "View folder",
24305 onClick: () => {
24306 const winId = `desktop-mode-folder-${targetParentId}`;
24307 const mgr = window.desktopMode?.windowManager;
24308 if (mgr?.focus) {
24309 const w = mgr.focus(winId);
24310 if (w) {
24311 return;
24312 }
24313 }
24314 if (mgr?.open) {
24315 void mgr.open(winId);
24316 }
24317 }
24318 };
24319 }
24320 showToast({
24321 message: reason,
24322 action,
24323 duration: 7e3
24324 });
24325 }
24326 const handlers$1 = /* @__PURE__ */ new Map();
24327 function registerCanvasPayloadHandler(type, handler) {
24328 handlers$1.set(type, handler);
24329 return () => {
24330 if (handlers$1.get(type) === handler) {
24331 handlers$1.delete(type);
24332 }
24333 };
24334 }
24335 function canvasPayloadAccepts(payload, ctx) {
24336 const handler = handlers$1.get(payload.type);
24337 return handler ? handler.accept(payload.data, ctx) : false;
24338 }
24339 function canvasPayloadDrop(session, ev, ctx) {
24340 const handler = handlers$1.get(session.payload.type);
24341 if (!handler) {
24342 return false;
24343 }
24344 handler.onDrop(session, ev, ctx);
24345 return true;
24346 }
24347 function broadcastFilesChange(kind, action, ids) {
24348 const api = window.wp?.desktop;
24349 api?.broadcast?.(`desktop-mode.${kind}.changed`, {
24350 source: "desktop-files",
24351 action,
24352 ids
24353 });
24354 }
24355 function showTrashErrorToast(err) {
24356 const api = window.wp?.desktop;
24357 if (!api?.showToast) {
24358 return;
24359 }
24360 const raw = err instanceof Error ? err.message : String(err);
24361 const friendly = raw.replace(/^\[desktop-mode\][^:]*:\s*/, "").replace(/^desktop_mode_files_[a-z_]+\s*/, "");
24362 api.showToast({
24363 message: friendly || "Could not move this item to the recycle bin.",
24364 duration: 5e3
24365 });
24366 }
24367 function showTrashedToast(message, onUndo) {
24368 const api = window.wp?.desktop;
24369 if (!api?.showToast) {
24370 return;
24371 }
24372 api.showToast({
24373 message,
24374 duration: 6e3,
24375 action: {
24376 label: "Undo",
24377 onClick: onUndo
24378 }
24379 });
24380 }
24381 async function trashPlacementWithUndo(placement) {
24382 const placementId = placement.id;
24383 const parentId = placement.parentId;
24384 const title = placement.file?.title ?? "Item";
24385 const kind = placement.file?.type === "shortcut" ? "shortcut" : "placement";
24386 store.removePlacement(placementId);
24387 try {
24388 await deletePlacement(placementId);
24389 broadcastFilesChange(kind, "trashed", [placementId]);
24390 showTrashedToast(`"${title}" moved to Trash`, async () => {
24391 try {
24392 await restoreTrashedItem(placementId, "placement");
24393 const res = await listPlacements(parentId);
24394 store.setFolderPlacements(parentId, res.placements);
24395 broadcastFilesChange(kind, "untrashed", [placementId]);
24396 } catch (err) {
24397 console.error("[desktop-mode] restore failed:", err);
24398 }
24399 });
24400 } catch (err) {
24401 console.error("[desktop-mode] deletePlacement failed:", err);
24402 showTrashErrorToast(err);
24403 void listPlacements(parentId).then((res) => {
24404 store.setFolderPlacements(parentId, res.placements);
24405 });
24406 }
24407 }
24408 async function trashFolderWithUndo(placement) {
24409 const folderId = parseInt(placement.file.ref, 10);
24410 if (!folderId) {
24411 return;
24412 }
24413 const placementId = placement.id;
24414 const parentId = placement.parentId;
24415 const title = placement.file?.title ?? "Folder";
24416 store.removePlacement(placementId);
24417 store.removeFolder(folderId);
24418 try {
24419 await deleteFolder(folderId);
24420 broadcastFilesChange("folder", "trashed", [folderId]);
24421 showTrashedToast(`"${title}" moved to Trash`, async () => {
24422 try {
24423 await restoreTrashedItem(folderId, "folder");
24424 const res = await listPlacements(parentId);
24425 store.setFolderPlacements(parentId, res.placements);
24426 broadcastFilesChange("folder", "untrashed", [folderId]);
24427 } catch (err) {
24428 console.error("[desktop-mode] restore folder failed:", err);
24429 }
24430 });
24431 } catch (err) {
24432 console.error("[desktop-mode] deleteFolder failed:", err);
24433 showTrashErrorToast(err);
24434 void listPlacements(parentId).then((res) => {
24435 store.setFolderPlacements(parentId, res.placements);
24436 });
24437 }
24438 }
24439 function trashByFileType(placement) {
24440 if (placement.file?.type === "folder") {
24441 return trashFolderWithUndo(placement);
24442 }
24443 return trashPlacementWithUndo(placement);
24444 }
24445 function buildBridgePayloadFromPlacement(placement) {
24446 const file = placement.file;
24447 if (!file) {
24448 return void 0;
24449 }
24450 const id = parseInt(String(file.ref ?? ""), 10);
24451 if (!Number.isFinite(id) || id <= 0) {
24452 return void 0;
24453 }
24454 const title = String(file.title ?? "");
24455 if (file.type === "attachment") {
24456 const url = String(file.sourceUrl ?? file.previewUrl ?? "");
24457 return {
24458 kind: "attachment",
24459 id,
24460 url,
24461 title,
24462 alt: String(file.alt ?? ""),
24463 mime: String(file.mime ?? ""),
24464 thumbnailUrl: file.previewUrl ? String(file.previewUrl) : void 0
24465 };
24466 }
24467 if (file.type === "post") {
24468 return {
24469 kind: "post",
24470 id,
24471 postType: String(file.postType ?? "post"),
24472 url: String(file.link ?? ""),
24473 title
24474 };
24475 }
24476 if (file.type === "user") {
24477 return {
24478 kind: "user",
24479 id,
24480 url: String(file.link ?? ""),
24481 title
24482 };
24483 }
24484 return void 0;
24485 }
24486 function getDragManager$2() {
24487 const api = window.wp?.desktop?.dragManager;
24488 return api ?? null;
24489 }
24490 const LAYER_CLASS = "desktop-mode-files-layer";
24491 function mountFilesLayer(host, folderId = 0) {
24492 const container = document.createElement("div");
24493 container.className = LAYER_CLASS;
24494 container.setAttribute("role", "list");
24495 container.dataset.folderId = String(folderId);
24496 host.appendChild(container);
24497 let lastFingerprint = "";
24498 let selectedId = null;
24499 const selectionListeners = /* @__PURE__ */ new Set();
24500 const notifySelection = (placement) => {
24501 for (const cb of selectionListeners) {
24502 try {
24503 cb(placement);
24504 } catch (err) {
24505 console.error(
24506 "[desktop-mode] files: selection listener threw:",
24507 err
24508 );
24509 }
24510 }
24511 };
24512 const setSelected = (placement) => {
24513 const newId = placement ? placement.id : null;
24514 if (newId === selectedId) {
24515 return;
24516 }
24517 container.querySelectorAll(`.${TILE_CLASS}--selected`).forEach((n) => n.removeAttribute("selected"));
24518 if (placement) {
24519 const tile2 = container.querySelector(
24520 `[data-placement-id="${placement.id}"]`
24521 );
24522 tile2?.setAttribute("selected", "");
24523 }
24524 selectedId = newId;
24525 notifySelection(placement);
24526 };
24527 const computeLayout = (list2) => {
24528 const pinnedSlots = /* @__PURE__ */ new Map();
24529 const occupiedCells = /* @__PURE__ */ new Set();
24530 let pinnedIdx = 0;
24531 for (const placement of list2) {
24532 if (!isPinned(placement)) {
24533 continue;
24534 }
24535 const slot = cellToPos(0, pinnedIdx);
24536 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
24537 occupiedCells.add(cellKey(slot.col, slot.row));
24538 pinnedIdx += 1;
24539 }
24540 const displaced = /* @__PURE__ */ new Map();
24541 for (const placement of list2) {
24542 if (pinnedSlots.has(placement.id)) {
24543 continue;
24544 }
24545 const target2 = pointToCell(placement.x, placement.y);
24546 const key = cellKey(target2.col, target2.row);
24547 if (!occupiedCells.has(key)) {
24548 occupiedCells.add(key);
24549 continue;
24550 }
24551 const free = snapToEmptyCell(
24552 placement.x,
24553 placement.y,
24554 occupiedCells,
24555 host
24556 );
24557 occupiedCells.add(cellKey(free.col, free.row));
24558 displaced.set(placement.id, { x: free.x, y: free.y });
24559 }
24560 return { pinnedSlots, displaced };
24561 };
24562 const applyTilePosition = (tile2, placement, pinnedSlots, displaced) => {
24563 const pinned = pinnedSlots.get(placement.id);
24564 const moved = displaced.get(placement.id);
24565 if (pinned) {
24566 setTilePosition(tile2, pinned.x, pinned.y);
24567 } else if (moved) {
24568 setTilePosition(tile2, moved.x, moved.y);
24569 } else {
24570 setTilePosition(tile2, placement.x, placement.y);
24571 }
24572 };
24573 const wireTile = (placement, pinnedSlots, displaced) => {
24574 const tile2 = buildTile(placement, folderId);
24575 const pinnedSlot = pinnedSlots.get(placement.id);
24576 if (pinnedSlot) {
24577 setTilePosition(tile2, pinnedSlot.x, pinnedSlot.y);
24578 tile2.classList.add(`${TILE_CLASS}--pinned`);
24579 attachContextMenu(tile2, placement);
24580 attachSelectOnClick(tile2, placement);
24581 if (shouldRejectTileDrops(placement)) {
24582 const dragManager = getDragManager$2();
24583 if (dragManager) {
24584 tileRejectDeregisters.set(
24585 placement.id,
24586 registerTileRejectTarget(dragManager, tile2, placement)
24587 );
24588 }
24589 }
24590 return tile2;
24591 }
24592 const moved = displaced.get(placement.id);
24593 if (moved) {
24594 setTilePosition(tile2, moved.x, moved.y);
24595 }
24596 attachTileDrag(tile2, placement, folderId);
24597 attachContextMenu(tile2, placement);
24598 attachSelectOnClick(tile2, placement);
24599 if (placement.file.type === "folder") {
24600 const targetFolderId = parseInt(placement.file.ref, 10);
24601 if (targetFolderId > 0) {
24602 const dragManager = getDragManager$2();
24603 if (dragManager) {
24604 const deregister = registerFolderDropTarget(
24605 dragManager,
24606 tile2,
24607 targetFolderId
24608 );
24609 folderDropDeregisters.set(placement.id, deregister);
24610 }
24611 }
24612 } else if (shouldRejectTileDrops(placement)) {
24613 const dragManager = getDragManager$2();
24614 if (dragManager) {
24615 tileRejectDeregisters.set(
24616 placement.id,
24617 registerTileRejectTarget(dragManager, tile2, placement)
24618 );
24619 }
24620 }
24621 return tile2;
24622 };
24623 const tryPatchIncremental = (list2) => {
24624 const existing = /* @__PURE__ */ new Map();
24625 for (const tile2 of container.querySelectorAll(
24626 "[data-placement-id]"
24627 )) {
24628 const raw = tile2.dataset.placementId ?? "";
24629 const id = parseInt(raw, 10);
24630 if (raw === "" || Number.isNaN(id) && raw !== "-0") {
24631 return false;
24632 }
24633 existing.set(id, tile2);
24634 }
24635 const wantIds = /* @__PURE__ */ new Set();
24636 for (const placement of list2) {
24637 wantIds.add(placement.id);
24638 }
24639 for (const placement of list2) {
24640 const tile2 = existing.get(placement.id);
24641 if (!tile2) {
24642 continue;
24643 }
24644 if (tile2.dataset.fileType !== placement.file.type) {
24645 return false;
24646 }
24647 if (tile2.dataset.fileRef !== placement.file.ref) {
24648 return false;
24649 }
24650 const wasPinned = tile2.classList.contains(
24651 `${TILE_CLASS}--pinned`
24652 );
24653 if (wasPinned !== isPinned(placement)) {
24654 return false;
24655 }
24656 }
24657 for (const [id, tile2] of existing) {
24658 if (wantIds.has(id)) {
24659 continue;
24660 }
24661 const folderDereg = folderDropDeregisters.get(id);
24662 if (folderDereg) {
24663 try {
24664 folderDereg();
24665 } catch {
24666 }
24667 folderDropDeregisters.delete(id);
24668 }
24669 const rejectDereg = tileRejectDeregisters.get(id);
24670 if (rejectDereg) {
24671 try {
24672 rejectDereg();
24673 } catch {
24674 }
24675 tileRejectDeregisters.delete(id);
24676 }
24677 tile2.remove();
24678 }
24679 const { pinnedSlots, displaced } = computeLayout(list2);
24680 for (const placement of list2) {
24681 const tile2 = existing.get(placement.id);
24682 if (tile2) {
24683 applyTilePosition(tile2, placement, pinnedSlots, displaced);
24684 syncTileLabel(tile2, placement);
24685 continue;
24686 }
24687 container.appendChild(
24688 wireTile(placement, pinnedSlots, displaced)
24689 );
24690 }
24691 if (selectedId !== null && !container.querySelector(
24692 `[data-placement-id="${selectedId}"]`
24693 )) {
24694 selectedId = null;
24695 notifySelection(null);
24696 }
24697 doAction("desktop-mode.files.grid-rendered", {
24698 folderId,
24699 count: list2.length
24700 });
24701 return true;
24702 };
24703 const repaint = (state2) => {
24704 const raw = state2.placementsByFolder.get(folderId) ?? [];
24705 const list2 = raw.slice().sort((a, b) => {
24706 const ap = isPinned(a) ? 0 : 1;
24707 const bp = isPinned(b) ? 0 : 1;
24708 return ap - bp;
24709 });
24710 const fp = fingerprint(list2);
24711 if (fp === lastFingerprint) {
24712 return;
24713 }
24714 lastFingerprint = fp;
24715 if (tryPatchPositions(list2, container, host)) {
24716 return;
24717 }
24718 if (tryPatchIncremental(list2)) {
24719 return;
24720 }
24721 container.replaceChildren();
24722 for (const [, deregister] of folderDropDeregisters) {
24723 try {
24724 deregister();
24725 } catch {
24726 }
24727 }
24728 folderDropDeregisters.clear();
24729 for (const [, deregister] of tileRejectDeregisters) {
24730 try {
24731 deregister();
24732 } catch {
24733 }
24734 }
24735 tileRejectDeregisters.clear();
24736 const { pinnedSlots, displaced } = computeLayout(list2);
24737 for (const placement of list2) {
24738 container.appendChild(
24739 wireTile(placement, pinnedSlots, displaced)
24740 );
24741 }
24742 if (selectedId !== null && !container.querySelector(`[data-placement-id="${selectedId}"]`)) {
24743 selectedId = null;
24744 notifySelection(null);
24745 } else if (selectedId !== null) {
24746 const tile2 = container.querySelector(
24747 `[data-placement-id="${selectedId}"]`
24748 );
24749 tile2?.setAttribute("selected", "");
24750 }
24751 doAction("desktop-mode.files.grid-rendered", {
24752 folderId,
24753 count: list2.length
24754 });
24755 };
24756 const dropTargetDeregisters = [];
24757 const folderDropDeregisters = /* @__PURE__ */ new Map();
24758 const tileRejectDeregisters = /* @__PURE__ */ new Map();
24759 let dropPreviewEl = null;
24760 let dropPreviewMoveHandler = null;
24761 const installCanvasDropPreview = (session) => {
24762 if (dropPreviewEl) {
24763 return;
24764 }
24765 if (session.payload.type !== "desktop-file") {
24766 return;
24767 }
24768 const previewEl = document.createElement("div");
24769 previewEl.className = "desktop-mode-files-drop-preview";
24770 previewEl.setAttribute("aria-hidden", "true");
24771 container.appendChild(previewEl);
24772 dropPreviewEl = previewEl;
24773 const ghost = session.payload.ghost;
24774 const offsetX = ghost?.offsetX ?? 0;
24775 const offsetY = ghost?.offsetY ?? 0;
24776 const data = session.payload.data;
24777 const movingId = data?.placement?.id;
24778 const updatePreview = (clientX, clientY) => {
24779 const rect = container.getBoundingClientRect();
24780 const rawX = Math.max(0, clientX - rect.left - offsetX);
24781 const rawY = Math.max(0, clientY - rect.top - offsetY);
24782 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
24783 const occupied = buildVisualOccupiedSet(peers, movingId);
24784 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
24785 previewEl.style.transform = `translate3d(${cell.x}px, ${cell.y}px, 0)`;
24786 };
24787 const sourceRect = session.payload.source.getBoundingClientRect();
24788 updatePreview(
24789 sourceRect.left + offsetX,
24790 sourceRect.top + offsetY
24791 );
24792 const moveHandler = (ev) => {
24793 updatePreview(ev.clientX, ev.clientY);
24794 };
24795 document.addEventListener("pointermove", moveHandler);
24796 dropPreviewMoveHandler = moveHandler;
24797 };
24798 const teardownCanvasDropPreview = () => {
24799 if (dropPreviewMoveHandler) {
24800 document.removeEventListener("pointermove", dropPreviewMoveHandler);
24801 dropPreviewMoveHandler = null;
24802 }
24803 if (dropPreviewEl) {
24804 dropPreviewEl.remove();
24805 dropPreviewEl = null;
24806 }
24807 };
24808 const canvasDropTarget = {
24809 id: `desktop-mode-files-canvas-${folderId}`,
24810 element: host,
24811 accept: (payload) => {
24812 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
24813 return canvasPayloadAccepts(payload, { folderId, host });
24814 }
24815 if (folderId > 0 && payload.type === "desktop-file") {
24816 const data = payload.data;
24817 if (data.placement.file?.type === "folder") {
24818 const movingFolderId = parseInt(data.placement.file.ref, 10);
24819 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, folderId)) {
24820 return false;
24821 }
24822 }
24823 }
24824 return true;
24825 },
24826 onEnter: (session) => {
24827 host.setAttribute("data-files-drop-active", "");
24828 if (session.payload.type === "desktop-file" || session.payload.type === "shortcut") {
24829 installCanvasDropPreview(session);
24830 }
24831 },
24832 onLeave: () => {
24833 host.removeAttribute("data-files-drop-active");
24834 teardownCanvasDropPreview();
24835 },
24836 onDrop: (session, ev) => {
24837 host.removeAttribute("data-files-drop-active");
24838 teardownCanvasDropPreview();
24839 if (session.payload.type !== "desktop-file" && session.payload.type !== "shortcut") {
24840 canvasPayloadDrop(session, ev, { folderId, host });
24841 return;
24842 }
24843 const rect = container.getBoundingClientRect();
24844 const ghost = session.payload.ghost;
24845 const offsetX = ghost?.offsetX ?? 0;
24846 const offsetY = ghost?.offsetY ?? 0;
24847 const rawX = Math.max(0, ev.clientX - rect.left - offsetX);
24848 const rawY = Math.max(0, ev.clientY - rect.top - offsetY);
24849 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
24850 if (session.payload.type === "desktop-file") {
24851 const data = session.payload.data;
24852 const occupied = buildVisualOccupiedSet(peers, data.placement.id);
24853 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
24854 const next = {
24855 ...data.placement,
24856 x: cell.x,
24857 y: cell.y,
24858 parentId: folderId
24859 };
24860 store.upsertPlacement(next);
24861 doAction("desktop-mode.files.tile-manually-placed", {
24862 folderId,
24863 placementId: data.placement.id
24864 });
24865 if (isSyntheticPlacement(data.placement)) {
24866 const dockItemId = readSynthSource(data.placement);
24867 if (dockItemId) {
24868 persistDockPromotedPosition(
24869 dockItemId,
24870 cell.x,
24871 cell.y
24872 );
24873 }
24874 return;
24875 }
24876 void updatePlacement(
24877 data.placement.id,
24878 {
24879 x: cell.x,
24880 y: cell.y,
24881 parentId: folderId
24882 },
24883 data.placement.updatedAtMs
24884 ).then((server) => {
24885 store.upsertPlacement(server, "remote");
24886 }).catch((err) => {
24887 if (isConflict(err)) {
24888 showConflictToast(err);
24889 } else {
24890 console.error(
24891 "[desktop-mode] files: drag persist failed",
24892 err
24893 );
24894 }
24895 store.upsertPlacement(data.placement);
24896 });
24897 return;
24898 }
24899 if (session.payload.type === "shortcut") {
24900 const data = session.payload.data;
24901 const occupied = buildVisualOccupiedSet(peers);
24902 const cell = nextRowMajorCell(occupied, host);
24903 void createPlacement({
24904 parentId: folderId,
24905 type: data.kind,
24906 ref: data.ref,
24907 x: cell.x,
24908 y: cell.y
24909 }).then((placement) => {
24910 store.upsertPlacement(placement);
24911 doAction("desktop-mode.files.shortcut-dropped", {
24912 folderId,
24913 placement
24914 });
24915 }).catch((err) => {
24916 console.error(
24917 "[desktop-mode] shortcut drop failed:",
24918 err
24919 );
24920 });
24921 }
24922 }
24923 };
24924 const dragManagerForLayer = getDragManager$2();
24925 if (dragManagerForLayer) {
24926 dropTargetDeregisters.push(
24927 dragManagerForLayer.registerDropTarget(canvasDropTarget)
24928 );
24929 }
24930 const onCanvasClick = (e) => {
24931 if (e.target instanceof Element && e.target.closest(`.${TILE_CLASS}`)) {
24932 return;
24933 }
24934 setSelected(null);
24935 };
24936 host.addEventListener("click", onCanvasClick);
24937 function attachSelectOnClick(tile2, placement) {
24938 tile2.addEventListener("click", (e) => {
24939 e.stopPropagation();
24940 setSelected(store.currentPlacement(placement));
24941 });
24942 }
24943 repaint(store.getState());
24944 const off = store.subscribe(repaint);
24945 let resolveHydrated = () => void 0;
24946 const hydrated = new Promise((resolve2) => {
24947 resolveHydrated = resolve2;
24948 });
24949 if (!store.getState().hydratedFolders.has(folderId)) {
24950 void listPlacements(folderId).then((res) => {
24951 store.setFolderPlacements(folderId, res.placements);
24952 }).catch((err) => {
24953 console.error("[desktop-mode] files: failed to hydrate folder", folderId, err);
24954 }).finally(() => {
24955 resolveHydrated();
24956 });
24957 } else {
24958 queueMicrotask(resolveHydrated);
24959 }
24960 const colsForWidth = () => {
24961 const w = host.clientWidth > 0 ? host.clientWidth : 4 * GRID_CELL_W;
24962 return Math.max(1, Math.floor((w - GRID_PADDING) / GRID_CELL_W));
24963 };
24964 const sortPlacements = (list2, mode) => {
24965 const sorted = list2.slice();
24966 switch (mode) {
24967 case "name-asc":
24968 sorted.sort(
24969 (a, b) => a.file.title.localeCompare(b.file.title)
24970 );
24971 break;
24972 case "name-desc":
24973 sorted.sort(
24974 (a, b) => b.file.title.localeCompare(a.file.title)
24975 );
24976 break;
24977 case "date-asc":
24978 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
24979 break;
24980 case "date-desc":
24981 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
24982 break;
24983 }
24984 return sorted;
24985 };
24986 const sort = (mode) => {
24987 const live = store.getState().placementsByFolder.get(folderId);
24988 if (!live || live.length === 0) {
24989 return;
24990 }
24991 const pinned = live.filter((p) => isPinned(p));
24992 const draggable = live.filter((p) => !isPinned(p));
24993 const sorted = sortPlacements(draggable, mode);
24994 const cols = colsForWidth();
24995 const occupied = /* @__PURE__ */ new Set();
24996 for (let i = 0; i < pinned.length; i += 1) {
24997 occupied.add(cellKey(0, i));
24998 }
24999 let idx = 0;
25000 const nextCell = () => {
25001 while (true) {
25002 const row = Math.floor(idx / cols);
25003 const col = idx % cols;
25004 idx += 1;
25005 if (!occupied.has(cellKey(col, row))) {
25006 return { col, row };
25007 }
25008 }
25009 };
25010 sorted.forEach((p, i) => {
25011 const cell = nextCell();
25012 const x = GRID_PADDING + cell.col * GRID_CELL_W;
25013 const y = GRID_PADDING + cell.row * GRID_CELL_H;
25014 const next = {
25015 ...p,
25016 x,
25017 y,
25018 sortOrder: i
25019 };
25020 store.upsertPlacement(next);
25021 if (isSyntheticPlacement(p)) {
25022 return;
25023 }
25024 void updatePlacement(p.id, { x, y, sortOrder: i }).catch((err) => {
25025 console.error(
25026 "[desktop-mode] files: sort persist failed",
25027 err
25028 );
25029 });
25030 });
25031 };
25032 const reflow = () => {
25033 const live = store.getState().placementsByFolder.get(folderId);
25034 if (!live || live.length === 0) {
25035 return;
25036 }
25037 const w = host.clientWidth > 0 ? host.clientWidth : Infinity;
25038 const overflowing = live.some((p) => {
25039 const right = p.x + GRID_CELL_W;
25040 return right > w;
25041 });
25042 if (!overflowing) {
25043 return;
25044 }
25045 const cols = colsForWidth();
25046 const pinned = live.filter((p) => isPinned(p));
25047 const draggable = live.filter((p) => !isPinned(p));
25048 const occupied = /* @__PURE__ */ new Set();
25049 for (let i = 0; i < pinned.length; i += 1) {
25050 occupied.add(cellKey(0, i));
25051 }
25052 let idx = 0;
25053 const nextCell = () => {
25054 while (true) {
25055 const row = Math.floor(idx / cols);
25056 const col = idx % cols;
25057 idx += 1;
25058 if (!occupied.has(cellKey(col, row))) {
25059 return { col, row };
25060 }
25061 }
25062 };
25063 for (const p of draggable) {
25064 const cell = nextCell();
25065 const x = GRID_PADDING + cell.col * GRID_CELL_W;
25066 const y = GRID_PADDING + cell.row * GRID_CELL_H;
25067 const tile2 = container.querySelector(
25068 `[data-placement-id="${p.id}"]`
25069 );
25070 if (tile2) {
25071 setTilePosition(tile2, x, y);
25072 }
25073 }
25074 };
25075 let lastWidth = host.clientWidth;
25076 let resizeObserver = null;
25077 if (typeof ResizeObserver !== "undefined") {
25078 resizeObserver = new ResizeObserver(() => {
25079 const w = host.clientWidth;
25080 if (w === lastWidth) {
25081 return;
25082 }
25083 lastWidth = w;
25084 reflow();
25085 });
25086 resizeObserver.observe(host);
25087 }
25088 return {
25089 host,
25090 folderId,
25091 onSelectionChange(cb) {
25092 selectionListeners.add(cb);
25093 return () => {
25094 selectionListeners.delete(cb);
25095 };
25096 },
25097 sort,
25098 reflow,
25099 hydrated,
25100 dispose() {
25101 off();
25102 resizeObserver?.disconnect();
25103 resizeObserver = null;
25104 for (const deregister of dropTargetDeregisters) {
25105 try {
25106 deregister();
25107 } catch {
25108 }
25109 }
25110 dropTargetDeregisters.length = 0;
25111 for (const deregister of folderDropDeregisters.values()) {
25112 try {
25113 deregister();
25114 } catch {
25115 }
25116 }
25117 folderDropDeregisters.clear();
25118 for (const deregister of tileRejectDeregisters.values()) {
25119 try {
25120 deregister();
25121 } catch {
25122 }
25123 }
25124 tileRejectDeregisters.clear();
25125 host.removeEventListener("click", onCanvasClick);
25126 selectionListeners.clear();
25127 container.remove();
25128 }
25129 };
25130 }
25131 function fingerprint(list2) {
25132 if (list2.length === 0) {
25133 return "0";
25134 }
25135 const parts = [];
25136 for (const p of list2) {
25137 parts.push(
25138 `${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}`
25139 );
25140 }
25141 return parts.join("|");
25142 }
25143 function isPinned(placement) {
25144 return Boolean(placement.file.pinned);
25145 }
25146 function readSynthSource(placement) {
25147 const meta = placement.meta;
25148 if (!meta || typeof meta !== "object") {
25149 return null;
25150 }
25151 const v = meta.__synthFromDockItem;
25152 return typeof v === "string" && v !== "" ? v : null;
25153 }
25154 function isSyntheticPlacement(placement) {
25155 return placement.id <= 0 || readSynthSource(placement) !== null;
25156 }
25157 const RECYCLE_BIN_REF = "desktop-mode-recycle-bin";
25158 function shouldRejectTileDrops(placement) {
25159 if (placement.file?.type === "folder") {
25160 return false;
25161 }
25162 if (placement.file?.ref === RECYCLE_BIN_REF) {
25163 return false;
25164 }
25165 return true;
25166 }
25167 function buildVisualOccupiedSet(placements, excludeId) {
25168 const sorted = placements.slice().sort((a, b) => {
25169 const ap = isPinned(a) ? 0 : 1;
25170 const bp = isPinned(b) ? 0 : 1;
25171 return ap - bp;
25172 });
25173 const set = /* @__PURE__ */ new Set();
25174 let pinnedIdx = 0;
25175 for (const p of sorted) {
25176 if (excludeId !== void 0 && p.id === excludeId) {
25177 continue;
25178 }
25179 if (isPinned(p)) {
25180 set.add(cellKey(0, pinnedIdx));
25181 pinnedIdx += 1;
25182 } else {
25183 const cell = pointToCell(p.x, p.y);
25184 set.add(cellKey(cell.col, cell.row));
25185 }
25186 }
25187 return set;
25188 }
25189 function wouldCreateFolderCycle(movingFolderId, targetParentId) {
25190 if (targetParentId <= 0 || movingFolderId <= 0) {
25191 return false;
25192 }
25193 if (movingFolderId === targetParentId) {
25194 return true;
25195 }
25196 const parentByFolderId = /* @__PURE__ */ new Map();
25197 const state2 = store.getState();
25198 for (const bucket2 of state2.placementsByFolder.values()) {
25199 for (const p of bucket2) {
25200 if (p.file?.type !== "folder") {
25201 continue;
25202 }
25203 const fid = parseInt(p.file.ref, 10);
25204 if (Number.isNaN(fid) || fid <= 0) {
25205 continue;
25206 }
25207 if (!parentByFolderId.has(fid)) {
25208 parentByFolderId.set(fid, p.parentId);
25209 }
25210 }
25211 }
25212 const visited = /* @__PURE__ */ new Set();
25213 let cursor = targetParentId;
25214 let maxDepth = 256;
25215 while (cursor > 0 && maxDepth-- > 0) {
25216 if (cursor === movingFolderId) {
25217 return true;
25218 }
25219 if (visited.has(cursor)) {
25220 return true;
25221 }
25222 visited.add(cursor);
25223 const next = parentByFolderId.get(cursor);
25224 if (next === void 0) {
25225 return false;
25226 }
25227 cursor = next;
25228 }
25229 return false;
25230 }
25231 function persistDockPromotedPosition(dockItemId, x, y) {
25232 const api = window.wp?.desktop;
25233 if (!api?.getOsSettings || !api?.updateOsSettings) {
25234 return;
25235 }
25236 const current = api.getOsSettings().dockPromotedPositions ?? {};
25237 api.updateOsSettings({
25238 dockPromotedPositions: {
25239 ...current,
25240 [dockItemId]: { x, y }
25241 }
25242 });
25243 }
25244 function tryPatchPositions(list2, container, host) {
25245 const tiles = Array.from(
25246 container.querySelectorAll("[data-placement-id]")
25247 );
25248 if (tiles.length !== list2.length) {
25249 return false;
25250 }
25251 const byId = /* @__PURE__ */ new Map();
25252 for (const tile2 of tiles) {
25253 const raw = tile2.dataset.placementId ?? "";
25254 const id = parseInt(raw, 10);
25255 if (raw === "" || Number.isNaN(id) && raw !== "-0") {
25256 return false;
25257 }
25258 byId.set(id, tile2);
25259 }
25260 for (const placement of list2) {
25261 const tile2 = byId.get(placement.id);
25262 if (!tile2) {
25263 return false;
25264 }
25265 if (tile2.dataset.fileType !== placement.file.type) {
25266 return false;
25267 }
25268 if (tile2.dataset.fileRef !== placement.file.ref) {
25269 return false;
25270 }
25271 const wasPinned = tile2.classList.contains(`${TILE_CLASS}--pinned`);
25272 if (wasPinned !== isPinned(placement)) {
25273 return false;
25274 }
25275 }
25276 const pinnedSlots = /* @__PURE__ */ new Map();
25277 const occupiedCells = /* @__PURE__ */ new Set();
25278 let pinnedIdx = 0;
25279 for (const placement of list2) {
25280 if (!isPinned(placement)) {
25281 continue;
25282 }
25283 const slot = cellToPos(0, pinnedIdx);
25284 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
25285 occupiedCells.add(cellKey(slot.col, slot.row));
25286 pinnedIdx += 1;
25287 }
25288 const displaced = /* @__PURE__ */ new Map();
25289 for (const placement of list2) {
25290 if (pinnedSlots.has(placement.id)) {
25291 continue;
25292 }
25293 const target2 = pointToCell(placement.x, placement.y);
25294 const key = cellKey(target2.col, target2.row);
25295 if (!occupiedCells.has(key)) {
25296 occupiedCells.add(key);
25297 continue;
25298 }
25299 const free = snapToEmptyCell(
25300 placement.x,
25301 placement.y,
25302 occupiedCells,
25303 host
25304 );
25305 occupiedCells.add(cellKey(free.col, free.row));
25306 displaced.set(placement.id, { x: free.x, y: free.y });
25307 }
25308 for (const placement of list2) {
25309 const tile2 = byId.get(placement.id);
25310 if (!tile2) {
25311 continue;
25312 }
25313 const pinned = pinnedSlots.get(placement.id);
25314 const disp = displaced.get(placement.id);
25315 if (pinned) {
25316 setTilePosition(tile2, pinned.x, pinned.y);
25317 } else if (disp) {
25318 setTilePosition(tile2, disp.x, disp.y);
25319 } else {
25320 setTilePosition(tile2, placement.x, placement.y);
25321 }
25322 syncTileLabel(tile2, placement);
25323 }
25324 return true;
25325 }
25326 function syncTileLabel(tile2, placement) {
25327 const label = placementLabel(placement);
25328 if (tile2.getAttribute("label") !== label) {
25329 tile2.setAttribute("label", label);
25330 }
25331 }
25332 function hidePromotedDockItem(dockItemId) {
25333 const api = window.wp?.desktop;
25334 if (!api?.getOsSettings || !api?.updateOsSettings) {
25335 return;
25336 }
25337 const current = api.getOsSettings().itemVisibility ?? {};
25338 const next = { ...current, [dockItemId]: "dock" };
25339 api.updateOsSettings({ itemVisibility: next });
25340 }
25341 function registerTileRejectTarget(dragManager, tile2, placement) {
25342 const ctx = { placement };
25343 let hoveredType = null;
25344 return dragManager.registerDropTarget({
25345 id: `desktop-mode-files-tile-${placement.id}-reject`,
25346 element: tile2,
25347 get acceptLabel() {
25348 return hoveredType ? tilePayloadAcceptLabel(hoveredType, ctx) : void 0;
25349 },
25350 accept: (payload) => {
25351 hoveredType = payload.type;
25352 return tilePayloadAccepts(payload, ctx);
25353 },
25354 onEnter: () => {
25355 tile2.classList.add(`${TILE_CLASS}--drop-target`);
25356 },
25357 onLeave: () => {
25358 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
25359 },
25360 onDrop: (session, ev) => {
25361 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
25362 tilePayloadDrop(session, ev, ctx);
25363 }
25364 });
25365 }
25366 function registerFolderDropTarget(dragManager, tile2, targetFolderId, currentFolderId) {
25367 const target2 = {
25368 id: `desktop-mode-files-folder-${targetFolderId}-tile-${tile2.dataset.placementId ?? "?"}`,
25369 element: tile2,
25370 accept: (payload) => {
25371 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
25372 return false;
25373 }
25374 if (payload.type === "desktop-file") {
25375 const data = payload.data;
25376 if (data.placement.file.type === "folder" && parseInt(data.placement.file.ref, 10) === targetFolderId) {
25377 return false;
25378 }
25379 if (data.placement.parentId === targetFolderId) {
25380 return false;
25381 }
25382 if (isSyntheticPlacement(data.placement)) {
25383 return false;
25384 }
25385 if (data.placement.file.type === "folder") {
25386 const movingFolderId = parseInt(data.placement.file.ref, 10);
25387 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, targetFolderId)) {
25388 return false;
25389 }
25390 }
25391 }
25392 return true;
25393 },
25394 onEnter: () => {
25395 tile2.classList.add(`${TILE_CLASS}--drop-target`);
25396 },
25397 onLeave: () => {
25398 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
25399 },
25400 onDrop: (session) => {
25401 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
25402 if (session.payload.type === "desktop-file") {
25403 const data = session.payload.data;
25404 const next = {
25405 ...data.placement,
25406 parentId: targetFolderId
25407 };
25408 store.upsertPlacement(next);
25409 void updatePlacement(
25410 data.placement.id,
25411 { parentId: targetFolderId },
25412 data.placement.updatedAtMs
25413 ).then((server) => {
25414 store.upsertPlacement(server, "remote");
25415 }).catch((err) => {
25416 if (isConflict(err)) {
25417 showConflictToast(err);
25418 } else {
25419 console.error(
25420 "[desktop-mode] files: move-into-folder persist failed",
25421 err
25422 );
25423 }
25424 store.upsertPlacement(data.placement);
25425 });
25426 return;
25427 }
25428 if (session.payload.type === "shortcut") {
25429 const data = session.payload.data;
25430 const peers = store.getState().placementsByFolder.get(targetFolderId) ?? [];
25431 const cell = nextRowMajorCell(buildVisualOccupiedSet(peers));
25432 void createPlacement({
25433 parentId: targetFolderId,
25434 type: data.kind,
25435 ref: data.ref,
25436 x: cell.x,
25437 y: cell.y
25438 }).then((placement) => {
25439 store.upsertPlacement(placement);
25440 doAction("desktop-mode.files.shortcut-dropped", {
25441 folderId: targetFolderId,
25442 placement
25443 });
25444 }).catch((err) => {
25445 console.error(
25446 "[desktop-mode] shortcut drop into folder failed:",
25447 err
25448 );
25449 });
25450 }
25451 }
25452 };
25453 return dragManager.registerDropTarget(target2);
25454 }
25455 function attachTileDrag(tile2, placement, folderId) {
25456 tile2.addEventListener("pointerdown", (e) => {
25457 if (e.button !== 0) {
25458 return;
25459 }
25460 const dragManager = getDragManager$2();
25461 if (!dragManager) {
25462 return;
25463 }
25464 const liveBucket = store.getState().placementsByFolder.get(folderId);
25465 const livePlacement = liveBucket?.find((p) => p.id === placement.id) ?? placement;
25466 parseFloat(tile2.style.left) || livePlacement.x;
25467 parseFloat(tile2.style.top) || livePlacement.y;
25468 dragManager.start({
25469 payload: {
25470 type: "desktop-file",
25471 source: tile2,
25472 data: {
25473 placement: livePlacement,
25474 sourceFolderId: folderId,
25475 // Synthesize a cross-frame bridge payload from the
25476 // placement's file shape so a wallpaper-placed
25477 // shortcut can be dropped into an open Gutenberg
25478 // iframe and inserted as the matching block. The
25479 // PHP serialize() methods (`Desktop_Mode_Post_File`,
25480 // `Desktop_Mode_User_File`, `Desktop_Mode_Attachment_File`)
25481 // surface the URL fields this needs.
25482 bridgePayload: buildBridgePayloadFromPlacement(livePlacement)
25483 },
25484 ghost: {
25485 offsetX: e.clientX - tile2.getBoundingClientRect().left,
25486 offsetY: e.clientY - tile2.getBoundingClientRect().top
25487 }
25488 },
25489 origin: e
25490 // `onClickOnly` intentionally empty — a tile click is
25491 // handled by the dedicated `attachSelectOnClick` listener
25492 // below, which fires from the regular `click` event after
25493 // a sub-threshold pointerup. The manager won't fire a
25494 // `click` itself; the browser does.
25495 });
25496 });
25497 }
25498 function attachContextMenu(tile2, wiredPlacement) {
25499 tile2.addEventListener("contextmenu", (e) => {
25500 e.preventDefault();
25501 e.stopPropagation();
25502 const placement = store.currentPlacement(wiredPlacement);
25503 const items = [
25504 {
25505 id: "open",
25506 label: "Open",
25507 icon: "dashicons-external",
25508 sort: 10,
25509 onClick: () => {
25510 const file = resolve(placement.file);
25511 void openFile(file);
25512 }
25513 }
25514 ];
25515 if (placement.file.type === "post") {
25516 items.push({
25517 id: "navigate-into",
25518 label: "Navigate into",
25519 icon: "dashicons-category",
25520 sort: 20,
25521 onClick: () => {
25522 const postId = parseInt(placement.file.ref, 10);
25523 if (!postId) {
25524 return;
25525 }
25526 const api = window.wp?.desktop?.myWordpress;
25527 const postType = typeof placement.file.postType === "string" ? placement.file.postType : "post";
25528 const entityId = postType === "page" ? "pages" : "posts";
25529 api?.openDetail({
25530 entityId,
25531 postId,
25532 postTitle: placement.file.title || `#${postId}`
25533 });
25534 }
25535 });
25536 }
25537 const isFolder = placement.file.type === "folder";
25538 if (isFolder) {
25539 items.push({
25540 id: "rename-folder",
25541 label: "Rename…",
25542 icon: "dashicons-edit",
25543 sort: 30,
25544 onClick: () => {
25545 const folderId = parseInt(placement.file.ref, 10);
25546 if (!folderId) {
25547 return;
25548 }
25549 openCreateFolderDialog({
25550 title: "Rename folder",
25551 label: "New name",
25552 submitLabel: "Rename",
25553 initialName: placement.file.title,
25554 onSubmit: async (name) => {
25555 const trimmed = name.trim();
25556 if (!trimmed || trimmed === placement.file.title) {
25557 return;
25558 }
25559 const previousTitle = placement.file.title;
25560 const optimistic = {
25561 ...placement,
25562 file: { ...placement.file, title: trimmed }
25563 };
25564 store.upsertPlacement(optimistic);
25565 try {
25566 const folderUpdatedAtMs = store.getState().folders.get(folderId)?.updatedAtMs ?? 0;
25567 const updated = await updateFolder(
25568 folderId,
25569 { name: trimmed },
25570 folderUpdatedAtMs
25571 );
25572 store.upsertFolder(updated);
25573 const refreshed = await listPlacements(
25574 placement.parentId
25575 );
25576 store.setFolderPlacements(
25577 placement.parentId,
25578 refreshed.placements
25579 );
25580 } catch (err) {
25581 console.error(
25582 "[desktop-mode] rename folder failed:",
25583 err
25584 );
25585 store.upsertPlacement({
25586 ...placement,
25587 file: {
25588 ...placement.file,
25589 title: previousTitle
25590 }
25591 });
25592 }
25593 }
25594 });
25595 }
25596 });
25597 if (placement.canTrash !== false) {
25598 items.push({
25599 id: "delete-folder",
25600 label: "Move folder to Trash",
25601 icon: "dashicons-trash",
25602 sort: 90,
25603 danger: true,
25604 onClick: () => trashFolderWithUndo(placement)
25605 });
25606 }
25607 } else {
25608 const synthFromDockItem = readSynthSource(placement);
25609 const isRegisteredIcon = placement.file.type === "shortcut";
25610 if (synthFromDockItem || isRegisteredIcon) {
25611 const hideId = synthFromDockItem ?? placement.file.ref;
25612 items.push({
25613 id: "hide-from-desktop",
25614 label: "Hide from desktop",
25615 icon: "dashicons-hidden",
25616 sort: 90,
25617 onClick: () => hidePromotedDockItem(hideId)
25618 });
25619 } else if (placement.canTrash !== false) {
25620 items.push({
25621 id: "remove",
25622 label: "Move to Trash",
25623 icon: "dashicons-trash",
25624 sort: 90,
25625 danger: true,
25626 onClick: () => trashPlacementWithUndo(placement)
25627 });
25628 }
25629 }
25630 openTileMenu({ x: e.clientX, y: e.clientY }, { placement, items });
25631 });
25632 }
25633 function formatBytes$1(bytes) {
25634 if (!Number.isFinite(bytes) || bytes <= 0) {
25635 return "0 B";
25636 }
25637 const units = ["B", "KB", "MB", "GB", "TB"];
25638 let v = bytes;
25639 let i = 0;
25640 while (v >= 1024 && i < units.length - 1) {
25641 v /= 1024;
25642 i++;
25643 }
25644 const decimals = v >= 100 || i === 0 ? 0 : 1;
25645 return `${v.toFixed(decimals)} ${units[i]}`;
25646 }
25647 const STATUS_BAR_CLASS = "desktop-mode-folder-status-bar";
25648 const ROOT_CLASS$2 = STATUS_BAR_CLASS;
25649 function mountFolderStatusBar(host, folderId) {
25650 const bar = document.createElement("div");
25651 bar.className = ROOT_CLASS$2;
25652 bar.setAttribute("role", "status");
25653 bar.dataset.folderId = String(folderId);
25654 host.appendChild(bar);
25655 const repaint = () => {
25656 const list2 = getFilesState().placementsByFolder.get(folderId) ?? [];
25657 const folders = list2.filter((p) => p.file.type === "folder").length;
25658 const files = list2.length - folders;
25659 let bytes = 0;
25660 for (const p of list2) {
25661 if (p.file.type === "upload") {
25662 const size = Number(
25663 p.file.sizeBytes ?? 0
25664 );
25665 if (Number.isFinite(size) && size > 0) {
25666 bytes += size;
25667 }
25668 }
25669 }
25670 const ctx = {
25671 folderId,
25672 totals: { files, folders, total: list2.length, bytes }
25673 };
25674 const segments = computeSegments(ctx);
25675 render(bar, segments);
25676 };
25677 repaint();
25678 const off = subscribeFilesStore(() => repaint());
25679 return {
25680 dispose() {
25681 off();
25682 bar.remove();
25683 }
25684 };
25685 }
25686 function computeSegments(ctx) {
25687 const { folders, files, bytes } = ctx.totals;
25688 const builtIns = [
25689 {
25690 id: "count",
25691 label: pluralize(files, "file", "files") + (folders > 0 ? `, ${pluralize(folders, "folder", "folders")}` : "") + // Stored-upload weight — only when the folder holds
25692 // real bytes (reference tiles weigh nothing).
25693 (bytes > 0 ? ` (${formatBytes$1(bytes)})` : ""),
25694 align: "start",
25695 sort: 10
25696 }
25697 ];
25698 const filtered = applyFilters(
25699 "desktop-mode.files.folder-window.status-bar",
25700 builtIns,
25701 ctx
25702 );
25703 return Array.isArray(filtered) ? filtered : builtIns;
25704 }
25705 function render(bar, segments) {
25706 const sort = (a, b) => {
25707 const sa = typeof a.sort === "number" ? a.sort : 100;
25708 const sb = typeof b.sort === "number" ? b.sort : 100;
25709 if (sa !== sb) {
25710 return sa - sb;
25711 }
25712 return a.label.localeCompare(b.label);
25713 };
25714 const start = segments.filter((s) => (s.align ?? "start") === "start").sort(sort);
25715 const end = segments.filter((s) => s.align === "end").sort(sort);
25716 bar.replaceChildren();
25717 bar.appendChild(buildCluster("start", start));
25718 bar.appendChild(buildCluster("end", end));
25719 }
25720 function buildCluster(align, segs) {
25721 const cluster = document.createElement("div");
25722 cluster.className = `${ROOT_CLASS$2}__cluster ${ROOT_CLASS$2}__cluster--${align}`;
25723 for (const seg of segs) {
25724 cluster.appendChild(buildSegment(seg));
25725 }
25726 return cluster;
25727 }
25728 function buildSegment(seg) {
25729 const interactive = typeof seg.onClick === "function";
25730 const el = document.createElement(interactive ? "button" : "span");
25731 el.className = `${ROOT_CLASS$2}__segment`;
25732 el.dataset.segmentId = seg.id;
25733 if (interactive) {
25734 el.type = "button";
25735 el.addEventListener("click", (e) => seg.onClick(e));
25736 }
25737 if (seg.icon) {
25738 const icon = document.createElement("span");
25739 icon.className = `${ROOT_CLASS$2}__icon dashicons ${seg.icon.replace(/[^a-zA-Z0-9_-]/g, "")}`;
25740 icon.setAttribute("aria-hidden", "true");
25741 el.appendChild(icon);
25742 }
25743 const label = document.createElement("span");
25744 label.className = `${ROOT_CLASS$2}__label`;
25745 label.textContent = seg.label;
25746 el.appendChild(label);
25747 return el;
25748 }
25749 function pluralize(n, singular, plural) {
25750 return `${n} ${n === 1 ? singular : plural}`;
25751 }
25752 const MENU_CLASS$1 = "desktop-mode-icon-canvas-menu";
25753 let activeMenu$1 = null;
25754 let activeFlyout = null;
25755 let activeCanvas = null;
25756 let outsideHandler = null;
25757 let escHandler = null;
25758 function attachIconCanvasMenu(canvas, deps2) {
25759 deps2.openOnBackgroundClick !== false;
25760 const onContextMenu = (e) => {
25761 if (isInsideTile(e.target) || isInsideMenu(e.target)) {
25762 return;
25763 }
25764 e.preventDefault();
25765 toggle(e.clientX, e.clientY);
25766 };
25767 let toggleGen = 0;
25768 const toggle = (x, y) => {
25769 if (activeCanvas === canvas && activeMenu$1) {
25770 closeMenu();
25771 return;
25772 }
25773 const items = buildItems(deps2);
25774 const filtered = applyFilters(
25775 "desktop-mode.icon-canvas.menu",
25776 items,
25777 deps2.scope
25778 );
25779 const finalItems = Array.isArray(filtered) ? filtered : items;
25780 const myGen = ++toggleGen;
25781 openWithShellOverlays(
25782 () => myGen === toggleGen,
25783 () => openMenu(finalItems, { x, y }, canvas)
25784 );
25785 };
25786 canvas.addEventListener("contextmenu", onContextMenu);
25787 return {
25788 dispose: () => {
25789 canvas.removeEventListener("contextmenu", onContextMenu);
25790 closeMenu();
25791 }
25792 };
25793 }
25794 function isInsideTile(target2) {
25795 if (!(target2 instanceof Element)) {
25796 return false;
25797 }
25798 return target2.closest(".desktop-mode-file-tile") !== null;
25799 }
25800 function isInsideMenu(target2) {
25801 if (!(target2 instanceof Element)) {
25802 return false;
25803 }
25804 return target2.closest(`.${MENU_CLASS$1}`) !== null;
25805 }
25806 function buildItems(deps2) {
25807 const sortItem = {
25808 id: "sort-by",
25809 label: __("Sort by", "desktop-mode"),
25810 icon: "dashicons-sort",
25811 sort: 10,
25812 children: [
25813 {
25814 id: "sort-name-asc",
25815 label: __("Name (A → Z)", "desktop-mode"),
25816 sort: 10,
25817 onClick: () => deps2.onSort("name-asc")
25818 },
25819 {
25820 id: "sort-name-desc",
25821 label: __("Name (Z → A)", "desktop-mode"),
25822 sort: 20,
25823 onClick: () => deps2.onSort("name-desc")
25824 },
25825 {
25826 id: "sort-date-desc",
25827 label: __("Newest first", "desktop-mode"),
25828 sort: 30,
25829 onClick: () => deps2.onSort("date-desc")
25830 },
25831 {
25832 id: "sort-date-asc",
25833 label: __("Oldest first", "desktop-mode"),
25834 sort: 40,
25835 onClick: () => deps2.onSort("date-asc")
25836 }
25837 ]
25838 };
25839 const items = [sortItem];
25840 if (Array.isArray(deps2.extraItems)) {
25841 items.push(...deps2.extraItems);
25842 }
25843 return items;
25844 }
25845 function sortItems(items) {
25846 return items.slice().sort((a, b) => {
25847 const sa = typeof a.sort === "number" ? a.sort : 100;
25848 const sb = typeof b.sort === "number" ? b.sort : 100;
25849 if (sa !== sb) {
25850 return sa - sb;
25851 }
25852 return a.label.localeCompare(b.label);
25853 });
25854 }
25855 function openMenu(items, pos, canvas) {
25856 closeMenu();
25857 if (items.length === 0) {
25858 return;
25859 }
25860 activeCanvas = canvas;
25861 const sorted = sortItems(items);
25862 const menu = document.createElement("wpd-context-menu");
25863 menu.setAttribute("open", "");
25864 menu.classList.add(MENU_CLASS$1);
25865 menu.style.left = `${pos.x}px`;
25866 menu.style.top = `${pos.y}px`;
25867 const itemById = /* @__PURE__ */ new Map();
25868 for (const item of sorted) {
25869 itemById.set(item.id, item);
25870 const opt = appendOption(menu, item);
25871 if (hasChildren(item)) {
25872 opt.addEventListener("mouseenter", () => {
25873 openFlyout(item, opt);
25874 });
25875 }
25876 }
25877 menu.addEventListener("wpd-context-menu-pick", (e) => {
25878 const detail = e.detail;
25879 const item = itemById.get(detail.id);
25880 if (!item) {
25881 return;
25882 }
25883 if (hasChildren(item)) {
25884 e.stopPropagation();
25885 const anchor = menu.querySelector(
25886 `[data-menu-item-id="${item.id}"]`
25887 );
25888 if (anchor) {
25889 openFlyout(item, anchor);
25890 }
25891 return;
25892 }
25893 closeMenu();
25894 item.onClick?.();
25895 });
25896 document.body.appendChild(menu);
25897 activeMenu$1 = menu;
25898 clampToViewport(menu);
25899 queueMicrotask(() => {
25900 outsideHandler = (e) => {
25901 if (isInsideMenu(e.target)) {
25902 return;
25903 }
25904 closeMenu();
25905 };
25906 escHandler = (e) => {
25907 if (e.key === "Escape") {
25908 closeMenu();
25909 }
25910 };
25911 document.addEventListener("mousedown", outsideHandler);
25912 document.addEventListener("keydown", escHandler);
25913 });
25914 }
25915 function appendOption(host, item) {
25916 const opt = document.createElement("wpd-context-menu-option");
25917 opt.dataset.menuItemId = item.id;
25918 opt.setAttribute("value", item.id);
25919 if (item.heading) {
25920 opt.setAttribute("heading", "");
25921 }
25922 if (item.disabled) {
25923 opt.setAttribute("disabled", "");
25924 }
25925 if (item.icon) {
25926 opt.setAttribute("icon", sanitizeClass$1(item.icon));
25927 }
25928 if (hasChildren(item)) {
25929 opt.setAttribute("has-children", "");
25930 }
25931 opt.textContent = item.label;
25932 host.appendChild(opt);
25933 return opt;
25934 }
25935 function openFlyout(parent, anchor) {
25936 closeFlyout();
25937 if (!hasChildren(parent)) {
25938 return;
25939 }
25940 const fly = document.createElement("wpd-context-menu");
25941 fly.setAttribute("open", "");
25942 fly.classList.add(MENU_CLASS$1, `${MENU_CLASS$1}--flyout`);
25943 const childById = /* @__PURE__ */ new Map();
25944 for (const child of sortItems(parent.children ?? [])) {
25945 childById.set(child.id, child);
25946 appendOption(fly, child);
25947 }
25948 fly.addEventListener("wpd-context-menu-pick", (e) => {
25949 const detail = e.detail;
25950 const child = childById.get(detail.id);
25951 if (!child) {
25952 return;
25953 }
25954 e.stopPropagation();
25955 closeMenu();
25956 child.onClick?.();
25957 });
25958 document.body.appendChild(fly);
25959 activeFlyout = fly;
25960 positionFlyout(fly, anchor);
25961 }
25962 function positionFlyout(fly, anchor) {
25963 const ar = anchor.getBoundingClientRect();
25964 fly.style.position = "fixed";
25965 fly.style.left = `${ar.right}px`;
25966 fly.style.top = `${ar.top}px`;
25967 const fr = fly.getBoundingClientRect();
25968 if (fr.right > window.innerWidth) {
25969 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
25970 }
25971 if (fr.bottom > window.innerHeight) {
25972 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
25973 }
25974 }
25975 function clampToViewport(menu) {
25976 const rect = menu.getBoundingClientRect();
25977 if (rect.right > window.innerWidth) {
25978 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
25979 }
25980 if (rect.bottom > window.innerHeight) {
25981 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
25982 }
25983 }
25984 function hasChildren(item) {
25985 return Array.isArray(item.children) && item.children.length > 0;
25986 }
25987 function closeFlyout() {
25988 if (activeFlyout) {
25989 activeFlyout.remove();
25990 activeFlyout = null;
25991 }
25992 }
25993 function closeMenu() {
25994 closeFlyout();
25995 if (activeMenu$1) {
25996 activeMenu$1.remove();
25997 activeMenu$1 = null;
25998 }
25999 activeCanvas = null;
26000 if (outsideHandler) {
26001 document.removeEventListener("mousedown", outsideHandler);
26002 outsideHandler = null;
26003 }
26004 if (escHandler) {
26005 document.removeEventListener("keydown", escHandler);
26006 escHandler = null;
26007 }
26008 }
26009 function sanitizeClass$1(raw) {
26010 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
26011 }
26012 const ROOT_CLASS$1 = "desktop-mode-breadcrumbs";
26013 function renderBreadcrumbs(host, segments, opts = {}) {
26014 host.replaceChildren();
26015 host.classList.add(ROOT_CLASS$1);
26016 if (opts.onBack) {
26017 const back = document.createElement("button");
26018 back.type = "button";
26019 back.className = `${ROOT_CLASS$1}__back`;
26020 back.setAttribute("aria-label", __("Back", "desktop-mode"));
26021 back.title = __("Back", "desktop-mode");
26022 const arrow = document.createElement("span");
26023 arrow.className = "dashicons dashicons-arrow-left-alt2";
26024 arrow.setAttribute("aria-hidden", "true");
26025 back.appendChild(arrow);
26026 if (opts.backDisabled) {
26027 back.disabled = true;
26028 }
26029 const onBack = opts.onBack;
26030 back.addEventListener("click", () => {
26031 if (back.disabled) {
26032 return;
26033 }
26034 onBack();
26035 });
26036 host.appendChild(back);
26037 }
26038 const nav = document.createElement("nav");
26039 nav.className = `${ROOT_CLASS$1}__crumbs`;
26040 nav.setAttribute("aria-label", __("Breadcrumb", "desktop-mode"));
26041 segments.forEach((seg, idx) => {
26042 if (idx > 0) {
26043 const sep = document.createElement("span");
26044 sep.className = `${ROOT_CLASS$1}__sep`;
26045 sep.setAttribute("aria-hidden", "true");
26046 sep.textContent = "›";
26047 nav.appendChild(sep);
26048 }
26049 if (!seg.onClick) {
26050 const here = document.createElement("span");
26051 here.className = `${ROOT_CLASS$1}__crumb ${ROOT_CLASS$1}__crumb--current`;
26052 here.setAttribute("aria-current", "page");
26053 here.textContent = seg.label;
26054 nav.appendChild(here);
26055 return;
26056 }
26057 const btn = document.createElement("button");
26058 btn.type = "button";
26059 btn.className = `${ROOT_CLASS$1}__crumb`;
26060 btn.textContent = seg.label;
26061 const onClick = seg.onClick;
26062 btn.addEventListener("click", () => {
26063 onClick();
26064 });
26065 nav.appendChild(btn);
26066 });
26067 host.appendChild(nav);
26068 }
26069 async function getJson(url, init2 = {}) {
26070 const response = await trackedFetch$1(url, {
26071 credentials: "same-origin",
26072 headers: {
26073 Accept: "application/json",
26074 "X-WP-Nonce": readRestNonce(),
26075 ...init2.headers ?? {}
26076 },
26077 ...init2
26078 });
26079 if (!response.ok) {
26080 throw new Error(`${response.status} ${response.statusText}`);
26081 }
26082 return await response.json();
26083 }
26084 function readRestNonce() {
26085 const cfg = window.wp?.desktop?.config;
26086 return cfg?.restNonce ?? "";
26087 }
26088 function readRestRoot() {
26089 const cfg = window.wp?.desktop?.config;
26090 if (cfg?.restUrl) {
26091 return cfg.restUrl.endsWith("/") ? cfg.restUrl : cfg.restUrl + "/";
26092 }
26093 return `${window.location.origin}/wp-json/`;
26094 }
26095 function restUrl(path) {
26096 return joinRestUrl(readRestRoot(), path);
26097 }
26098 function renderPlacementPreview(placement, host) {
26099 const filtered = applyFilters(
26100 "desktop-mode.files.preview",
26101 null,
26102 placement
26103 );
26104 if (filtered instanceof HTMLElement) {
26105 host.replaceChildren(filtered);
26106 return;
26107 }
26108 if (placement.accessGated) {
26109 host.replaceChildren(renderAccessGated(placement));
26110 return;
26111 }
26112 host.replaceChildren(renderLoading());
26113 void renderByType(placement).then((node) => {
26114 host.replaceChildren(node);
26115 }).catch((err) => {
26116 host.replaceChildren(renderError(err));
26117 });
26118 }
26119 function renderAccessGated(placement) {
26120 const wrap = document.createElement("div");
26121 wrap.className = "desktop-mode-files__access-gated";
26122 const ring = document.createElement("div");
26123 ring.className = "desktop-mode-files__access-gated-ring";
26124 const glyph = document.createElement("span");
26125 glyph.className = "dashicons dashicons-lock desktop-mode-files__access-gated-glyph";
26126 glyph.setAttribute("aria-hidden", "true");
26127 ring.appendChild(glyph);
26128 wrap.appendChild(ring);
26129 const title = document.createElement("h2");
26130 title.className = "desktop-mode-files__access-gated-title";
26131 title.textContent = "No permission to view";
26132 wrap.appendChild(title);
26133 const sub = document.createElement("p");
26134 sub.className = "desktop-mode-files__access-gated-sub";
26135 const target2 = placement.file.title || placement.file.type;
26136 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.`;
26137 wrap.appendChild(sub);
26138 const hint = document.createElement("p");
26139 hint.className = "desktop-mode-files__access-gated-hint";
26140 hint.textContent = "Ask the owner to grant access on the underlying item, or to remove it from the shared folder.";
26141 wrap.appendChild(hint);
26142 return wrap;
26143 }
26144 async function renderByType(placement) {
26145 const file = placement.file;
26146 switch (file.type) {
26147 case "post":
26148 return renderPostPreview(file.ref, file);
26149 case "folder":
26150 return renderFolderPreview(file);
26151 case "shortcut":
26152 return renderShortcutPreview(file);
26153 case "attachment":
26154 return renderAttachmentPreview(file.ref, file);
26155 case "user":
26156 return renderUserSummary(file.ref, file);
26157 case "term":
26158 return renderTermSummary(file);
26159 case "comment":
26160 return renderCommentSummary(file.ref, file);
26161 case "bookmark":
26162 return renderBookmarkPreview(file);
26163 default:
26164 return renderGenericPreview(file);
26165 }
26166 }
26167 async function renderPostPreview(ref, file) {
26168 const id = parseInt(ref, 10);
26169 if (!id) {
26170 return renderGenericPreview(file);
26171 }
26172 let data = null;
26173 for (const path of ["wp/v2/posts", "wp/v2/pages"]) {
26174 try {
26175 data = await getJson(
26176 restUrl(
26177 `${path}/${id}?_fields=id,title,content,date,link,status`
26178 )
26179 );
26180 break;
26181 } catch {
26182 }
26183 }
26184 if (!data) {
26185 return renderGenericPreview(file);
26186 }
26187 const wrap = articleShell();
26188 const h = document.createElement("h2");
26189 h.className = "desktop-mode-my-wordpress__article-title";
26190 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
26191 wrap.appendChild(h);
26192 const meta = document.createElement("p");
26193 meta.className = "desktop-mode-my-wordpress__article-meta";
26194 const parts = [];
26195 parts.push(formatDate(data.date));
26196 if (data.status && data.status !== "publish") {
26197 parts.push(data.status);
26198 }
26199 meta.textContent = parts.join(" · ");
26200 wrap.appendChild(meta);
26201 if (data.content?.rendered) {
26202 const body = document.createElement("div");
26203 body.className = "desktop-mode-my-wordpress__article-content";
26204 body.innerHTML = data.content.rendered;
26205 wrap.appendChild(body);
26206 }
26207 const footer = document.createElement("footer");
26208 footer.className = "desktop-mode-my-wordpress__article-footer";
26209 const myWordpressApi = window.wp?.desktop?.myWordpress;
26210 if (myWordpressApi) {
26211 const exploreBtn = document.createElement("wpd-button");
26212 exploreBtn.setAttribute("variant", "secondary");
26213 exploreBtn.textContent = __("Explore details", "desktop-mode");
26214 exploreBtn.title = __(
26215 "See author, comments, categories, tags, attached media, and revisions for this entry.",
26216 "desktop-mode"
26217 );
26218 exploreBtn.addEventListener("click", () => {
26219 const postType = typeof file.postType === "string" ? file.postType : "post";
26220 myWordpressApi.openDetail({
26221 entityId: postType === "page" ? "pages" : "posts",
26222 postId: id,
26223 postTitle: stripTags(data.title.rendered) || `#${id}`
26224 });
26225 });
26226 footer.appendChild(exploreBtn);
26227 }
26228 const editBtn = document.createElement("wpd-button");
26229 editBtn.setAttribute("variant", "primary");
26230 editBtn.textContent = __("Open in editor", "desktop-mode");
26231 editBtn.addEventListener("click", () => {
26232 const adminUrl = window.wp?.desktop?.config?.adminUrl;
26233 if (!adminUrl) {
26234 return;
26235 }
26236 const editUrl = `${adminUrl}post.php?post=${id}&action=edit`;
26237 const wm = window.wp?.desktop?.windowManager;
26238 const postType = typeof file.postType === "string" ? file.postType : "post";
26239 const entityId = postType === "page" ? "pages" : "posts";
26240 wm?.open({
26241 id: `${entityId}-edit-${id}`,
26242 url: editUrl,
26243 title: stripTags(data.title.rendered),
26244 icon: file.icon
26245 });
26246 });
26247 footer.appendChild(editBtn);
26248 wrap.appendChild(footer);
26249 return wrap;
26250 }
26251 async function renderUserSummary(ref, file) {
26252 const id = parseInt(ref, 10);
26253 if (!id) {
26254 return renderGenericPreview(file);
26255 }
26256 let data = null;
26257 try {
26258 data = await getJson(
26259 restUrl(`desktop-mode/v1/user-stats/${id}`)
26260 );
26261 } catch {
26262 return renderGenericPreview(file);
26263 }
26264 const wrap = articleShell("desktop-mode-my-wordpress__user");
26265 const header = document.createElement("header");
26266 header.className = "desktop-mode-my-wordpress__user-header";
26267 if (data.profile.avatarUrl) {
26268 const img = document.createElement("img");
26269 img.className = "desktop-mode-my-wordpress__user-avatar";
26270 img.src = data.profile.avatarUrl;
26271 img.alt = "";
26272 header.appendChild(img);
26273 }
26274 const head = document.createElement("div");
26275 head.className = "desktop-mode-my-wordpress__user-headline";
26276 const h = document.createElement("h2");
26277 h.className = "desktop-mode-my-wordpress__article-title";
26278 h.textContent = data.profile.name || file.title || `#${id}`;
26279 head.appendChild(h);
26280 if (data.profile.roleLabels && data.profile.roleLabels.length > 0) {
26281 const roles = document.createElement("div");
26282 roles.className = "desktop-mode-my-wordpress__user-roles";
26283 for (const r of data.profile.roleLabels) {
26284 const badge = document.createElement("span");
26285 badge.className = "desktop-mode-my-wordpress__user-role";
26286 badge.textContent = r;
26287 roles.appendChild(badge);
26288 }
26289 head.appendChild(roles);
26290 }
26291 header.appendChild(head);
26292 wrap.appendChild(header);
26293 if (data.profile.description) {
26294 const bio = document.createElement("div");
26295 bio.className = "desktop-mode-my-wordpress__user-bio";
26296 bio.textContent = data.profile.description;
26297 wrap.appendChild(bio);
26298 }
26299 const cards = document.createElement("div");
26300 cards.className = "desktop-mode-my-wordpress__user-stats";
26301 cards.appendChild(
26302 statCard(
26303 data.counts.posts.total.toLocaleString(),
26304 __("Posts", "desktop-mode")
26305 )
26306 );
26307 cards.appendChild(
26308 statCard(
26309 data.counts.pages.total.toLocaleString(),
26310 __("Pages", "desktop-mode")
26311 )
26312 );
26313 cards.appendChild(
26314 statCard(
26315 data.counts.commentsReceived.toLocaleString(),
26316 __("Comments received", "desktop-mode")
26317 )
26318 );
26319 wrap.appendChild(cards);
26320 return wrap;
26321 }
26322 async function renderTermSummary(file) {
26323 const id = parseInt(file.ref, 10);
26324 const taxonomy = typeof file.taxonomy === "string" && file.taxonomy ? file.taxonomy : "category";
26325 if (!id) {
26326 return renderGenericPreview(file);
26327 }
26328 let data = null;
26329 try {
26330 data = await getJson(
26331 restUrl(`desktop-mode/v1/term-stats/${taxonomy}/${id}`)
26332 );
26333 } catch {
26334 return renderGenericPreview(file);
26335 }
26336 const wrap = articleShell();
26337 const h = document.createElement("h2");
26338 h.className = "desktop-mode-my-wordpress__article-title";
26339 h.textContent = data.profile.name || file.title || `#${id}`;
26340 wrap.appendChild(h);
26341 const meta = document.createElement("p");
26342 meta.className = "desktop-mode-my-wordpress__article-meta";
26343 meta.textContent = data.profile.taxonomyLabel || data.profile.taxonomy;
26344 wrap.appendChild(meta);
26345 if (data.profile.description) {
26346 const desc = document.createElement("div");
26347 desc.className = "desktop-mode-my-wordpress__article-content";
26348 desc.innerHTML = data.profile.description;
26349 wrap.appendChild(desc);
26350 }
26351 const cards = document.createElement("div");
26352 cards.className = "desktop-mode-my-wordpress__user-stats";
26353 cards.appendChild(
26354 statCard(
26355 data.counts.posts.total.toLocaleString(),
26356 __("Posts", "desktop-mode")
26357 )
26358 );
26359 cards.appendChild(
26360 statCard(
26361 data.counts.commentsReceived.toLocaleString(),
26362 __("Comments", "desktop-mode")
26363 )
26364 );
26365 cards.appendChild(
26366 statCard(
26367 data.counts.distinctAuthors.toLocaleString(),
26368 __("Authors", "desktop-mode")
26369 )
26370 );
26371 wrap.appendChild(cards);
26372 return wrap;
26373 }
26374 async function renderCommentSummary(ref, file) {
26375 const id = parseInt(ref, 10);
26376 if (!id) {
26377 return renderGenericPreview(file);
26378 }
26379 let data = null;
26380 try {
26381 data = await getJson(
26382 restUrl(`desktop-mode/v1/comment-stats/${id}`)
26383 );
26384 } catch {
26385 return renderGenericPreview(file);
26386 }
26387 const wrap = articleShell();
26388 const header = document.createElement("header");
26389 header.className = "desktop-mode-my-wordpress__user-header";
26390 if (data.author.avatarUrl) {
26391 const img = document.createElement("img");
26392 img.className = "desktop-mode-my-wordpress__user-avatar";
26393 img.src = data.author.avatarUrl;
26394 img.alt = "";
26395 header.appendChild(img);
26396 }
26397 const head = document.createElement("div");
26398 head.className = "desktop-mode-my-wordpress__user-headline";
26399 const h = document.createElement("h2");
26400 h.className = "desktop-mode-my-wordpress__article-title";
26401 h.textContent = data.author.name;
26402 head.appendChild(h);
26403 const sub = document.createElement("p");
26404 sub.className = "desktop-mode-my-wordpress__article-meta";
26405 sub.textContent = `${formatDate(data.comment.date)} · ${data.comment.status}`;
26406 head.appendChild(sub);
26407 header.appendChild(head);
26408 wrap.appendChild(header);
26409 const body = document.createElement("div");
26410 body.className = "desktop-mode-my-wordpress__article-content";
26411 body.innerHTML = data.comment.rendered;
26412 wrap.appendChild(body);
26413 if (data.post) {
26414 const card = document.createElement("div");
26415 card.className = "desktop-mode-my-wordpress__comment-post";
26416 const link = document.createElement("a");
26417 link.className = "desktop-mode-my-wordpress__comment-post-title";
26418 link.href = data.post.link;
26419 link.target = "_blank";
26420 link.rel = "noopener noreferrer";
26421 link.textContent = data.post.title;
26422 card.appendChild(link);
26423 wrap.appendChild(card);
26424 }
26425 return wrap;
26426 }
26427 async function renderAttachmentPreview(ref, file) {
26428 const id = parseInt(ref, 10);
26429 if (!id) {
26430 return renderGenericPreview(file);
26431 }
26432 let data = null;
26433 try {
26434 data = await getJson(
26435 restUrl(
26436 `wp/v2/media/${id}?_fields=id,title,source_url,mime_type,alt_text,media_details`
26437 )
26438 );
26439 } catch {
26440 return renderGenericPreview(file);
26441 }
26442 const wrap = articleShell();
26443 const h = document.createElement("h2");
26444 h.className = "desktop-mode-my-wordpress__article-title";
26445 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
26446 wrap.appendChild(h);
26447 const meta = document.createElement("p");
26448 meta.className = "desktop-mode-my-wordpress__article-meta";
26449 meta.textContent = data.mime_type;
26450 wrap.appendChild(meta);
26451 if (data.mime_type.startsWith("image/")) {
26452 const img = document.createElement("img");
26453 img.className = "desktop-mode-my-wordpress__article-hero";
26454 const sizes = data.media_details?.sizes;
26455 img.src = sizes?.large?.source_url ?? sizes?.medium?.source_url ?? data.source_url;
26456 img.alt = data.alt_text ?? "";
26457 wrap.appendChild(img);
26458 } else {
26459 const p = document.createElement("p");
26460 const a = document.createElement("a");
26461 a.href = data.source_url;
26462 a.textContent = data.source_url;
26463 a.target = "_blank";
26464 a.rel = "noopener noreferrer";
26465 p.appendChild(a);
26466 wrap.appendChild(p);
26467 }
26468 return wrap;
26469 }
26470 function renderFolderPreview(file) {
26471 const wrap = articleShell();
26472 const h = document.createElement("h2");
26473 h.className = "desktop-mode-my-wordpress__article-title";
26474 h.textContent = file.title || __("(folder)", "desktop-mode");
26475 wrap.appendChild(h);
26476 const meta = document.createElement("p");
26477 meta.className = "desktop-mode-my-wordpress__article-meta";
26478 meta.textContent = __("Double-click to open.", "desktop-mode");
26479 wrap.appendChild(meta);
26480 return wrap;
26481 }
26482 function renderShortcutPreview(file) {
26483 const wrap = articleShell();
26484 const h = document.createElement("h2");
26485 h.className = "desktop-mode-my-wordpress__article-title";
26486 h.textContent = file.title || __("Shortcut", "desktop-mode");
26487 wrap.appendChild(h);
26488 const meta = document.createElement("p");
26489 meta.className = "desktop-mode-my-wordpress__article-meta";
26490 meta.textContent = __("Plugin shortcut. Double-click to open.", "desktop-mode");
26491 wrap.appendChild(meta);
26492 return wrap;
26493 }
26494 function renderBookmarkPreview(file) {
26495 const wrap = articleShell();
26496 const h = document.createElement("h2");
26497 h.className = "desktop-mode-my-wordpress__article-title";
26498 h.textContent = file.title || __("Bookmark", "desktop-mode");
26499 wrap.appendChild(h);
26500 const url = typeof file.url === "string" ? file.url : "";
26501 if (url) {
26502 const a = document.createElement("a");
26503 a.href = url;
26504 a.textContent = url;
26505 a.target = "_blank";
26506 a.rel = "noopener noreferrer";
26507 wrap.appendChild(a);
26508 }
26509 return wrap;
26510 }
26511 function renderGenericPreview(file) {
26512 const wrap = articleShell();
26513 const h = document.createElement("h2");
26514 h.className = "desktop-mode-my-wordpress__article-title";
26515 h.textContent = file.title || file.type;
26516 wrap.appendChild(h);
26517 const meta = document.createElement("p");
26518 meta.className = "desktop-mode-my-wordpress__article-meta";
26519 meta.textContent = sprintf(
26520 // translators: %s is a file-type slug.
26521 __("Type: %s", "desktop-mode"),
26522 file.type
26523 );
26524 wrap.appendChild(meta);
26525 if (!file.exists) {
26526 const warn2 = document.createElement("p");
26527 warn2.className = "desktop-mode-my-wordpress__article-meta";
26528 warn2.textContent = __(
26529 "The underlying entity is no longer available.",
26530 "desktop-mode"
26531 );
26532 wrap.appendChild(warn2);
26533 }
26534 return wrap;
26535 }
26536 function articleShell(extraClass = "") {
26537 const article = document.createElement("article");
26538 article.className = "desktop-mode-my-wordpress__article" + (extraClass ? " " + extraClass : "");
26539 return article;
26540 }
26541 function statCard(value, label) {
26542 const card = document.createElement("div");
26543 card.className = "desktop-mode-my-wordpress__user-stat";
26544 const v = document.createElement("span");
26545 v.className = "desktop-mode-my-wordpress__user-stat-value";
26546 v.textContent = value;
26547 card.appendChild(v);
26548 const l = document.createElement("span");
26549 l.className = "desktop-mode-my-wordpress__user-stat-label";
26550 l.textContent = label;
26551 card.appendChild(l);
26552 return card;
26553 }
26554 function renderLoading() {
26555 const wrap = document.createElement("div");
26556 wrap.className = "desktop-mode-my-wordpress__preview-loading";
26557 const spinner = document.createElement("wpd-spinner");
26558 wrap.appendChild(spinner);
26559 return wrap;
26560 }
26561 function renderError(err) {
26562 const wrap = document.createElement("div");
26563 wrap.className = "desktop-mode-my-wordpress__error";
26564 wrap.textContent = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
26565 return wrap;
26566 }
26567 function stripTags(html2) {
26568 const div = document.createElement("div");
26569 div.innerHTML = html2;
26570 return (div.textContent ?? "").trim();
26571 }
26572 function formatDate(iso) {
26573 if (!iso) {
26574 return "";
26575 }
26576 try {
26577 return new Date(iso).toLocaleString();
26578 } catch {
26579 return iso;
26580 }
26581 }
26582 function renderPreviewEmpty() {
26583 const wrap = document.createElement("div");
26584 wrap.className = "desktop-mode-my-wordpress__preview-empty";
26585 wrap.textContent = __(
26586 "Select an item to preview it here.",
26587 "desktop-mode"
26588 );
26589 return wrap;
26590 }
26591 const ID_PREFIX = "desktop-mode-embed-";
26592 const DEFAULT_W = 800;
26593 const DEFAULT_H = 600;
26594 const MIN_W = 360;
26595 const MIN_H = 240;
26596 const PADDING = 16;
26597 const lastPersisted = /* @__PURE__ */ new Map();
26598 function openEmbedWindow(file, ctx) {
26599 const url = file.ref();
26600 if (!url) {
26601 return;
26602 }
26603 const wm = window.wp?.desktop?.windowManager;
26604 if (!wm) {
26605 return;
26606 }
26607 const placement = ctx?.placement;
26608 const meta = placement?.meta ?? null;
26609 const windowId = placement ? `${ID_PREFIX}${placement.id}` : `${ID_PREFIX}anon-${hash(url)}`;
26610 const customName = meta?.name?.trim() ?? "";
26611 const title = customName !== "" ? customName : file.title();
26612 const cfg = {
26613 id: windowId,
26614 baseId: windowId,
26615 url,
26616 title,
26617 icon: file.icon(),
26618 minWidth: MIN_W,
26619 minHeight: MIN_H
26620 };
26621 const saved = meta?.window;
26622 const area = document.getElementById("desktop-mode-area");
26623 const aw = area?.clientWidth ?? window.innerWidth;
26624 const ah = area?.clientHeight ?? window.innerHeight;
26625 if (saved && Number.isFinite(saved.width) && Number.isFinite(saved.height)) {
26626 const { x, y, width, height } = clampGeometry(saved, aw, ah);
26627 cfg.x = x;
26628 cfg.y = y;
26629 cfg.width = width;
26630 cfg.height = height;
26631 } else {
26632 cfg.width = Math.min(DEFAULT_W, Math.max(MIN_W, aw - PADDING * 2));
26633 cfg.height = Math.min(DEFAULT_H, Math.max(MIN_H, ah - PADDING * 2));
26634 }
26635 if (placement) {
26636 if (saved) {
26637 lastPersisted.set(windowId, { ...saved });
26638 }
26639 }
26640 wm.open(cfg);
26641 }
26642 let installed = false;
26643 function installEmbedPersistence() {
26644 if (installed) {
26645 return;
26646 }
26647 installed = true;
26648 const onChange = (payload) => {
26649 const p = payload;
26650 const id = p?.windowId;
26651 if (!id || !id.startsWith(ID_PREFIX)) {
26652 return;
26653 }
26654 const placementIdStr = id.slice(ID_PREFIX.length);
26655 const placementId = parseInt(placementIdStr, 10);
26656 if (!placementId) {
26657 return;
26658 }
26659 const wm = window.wp?.desktop?.windowManager;
26660 const win = wm?.getById?.(id);
26661 const el = win?.element;
26662 if (!el) {
26663 return;
26664 }
26665 const next = {
26666 x: el.offsetLeft,
26667 y: el.offsetTop,
26668 width: el.offsetWidth,
26669 height: el.offsetHeight
26670 };
26671 const prev = lastPersisted.get(id);
26672 if (prev && prev.x === next.x && prev.y === next.y && prev.width === next.width && prev.height === next.height) {
26673 return;
26674 }
26675 lastPersisted.set(id, next);
26676 void persist(placementId, next);
26677 };
26678 addAction(HOOKS.WINDOW_DRAG_END, "desktop-mode-embed-persist", onChange);
26679 addAction(HOOKS.WINDOW_RESIZE_END, "desktop-mode-embed-persist", onChange);
26680 }
26681 async function persist(placementId, geo) {
26682 try {
26683 const list2 = await listPlacements(0);
26684 const row = list2.placements.find((p) => p.id === placementId);
26685 const prevMeta = row?.meta ?? {};
26686 const nextMeta = {
26687 ...prevMeta,
26688 window: geo
26689 };
26690 await updatePlacement(placementId, { meta: nextMeta });
26691 } catch (err) {
26692 console.warn("[desktop-mode] embed window persist failed:", err);
26693 }
26694 }
26695 function clampGeometry(g, areaW, areaH) {
26696 const width = Math.max(MIN_W, Math.min(g.width, areaW - PADDING));
26697 const height = Math.max(MIN_H, Math.min(g.height, areaH - PADDING));
26698 const x = Math.max(0, Math.min(g.x, Math.max(0, areaW - width)));
26699 const y = Math.max(0, Math.min(g.y, Math.max(0, areaH - height)));
26700 return { x, y, width, height };
26701 }
26702 function hash(s) {
26703 let h = 0;
26704 for (let i = 0; i < s.length; i++) {
26705 h = (Math.imul(h, 31) + s.charCodeAt(i)) % 2147483647;
26706 }
26707 return Math.abs(h).toString(36);
26708 }
26709 function navigateToDownload(url) {
26710 const a = document.createElement("a");
26711 a.href = url;
26712 a.setAttribute("download", "");
26713 a.style.display = "none";
26714 document.body.appendChild(a);
26715 a.click();
26716 a.remove();
26717 }
26718 function adminBase() {
26719 const cfg = window.wp?.desktop?.config;
26720 const url = cfg?.adminUrl ?? "/wp-admin/";
26721 return url.endsWith("/") ? url : `${url}/`;
26722 }
26723 function sanitizedWebUrl(file) {
26724 const url = typeof file.shape.url === "string" ? file.shape.url : "";
26725 if (!url) {
26726 return "";
26727 }
26728 try {
26729 const parsed = new URL(url, window.location.href);
26730 if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
26731 return "";
26732 }
26733 } catch {
26734 return "";
26735 }
26736 return url;
26737 }
26738 function registerBuiltInFileOpeners() {
26739 registerOpener({
26740 id: "wp-post-editor",
26741 label: "Block Editor",
26742 types: ["post"],
26743 isDefault: true,
26744 sort: 10,
26745 handler: {
26746 kind: "url",
26747 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
26748 }
26749 });
26750 registerOpener({
26751 id: "wp-media-editor",
26752 label: "Media editor",
26753 types: ["attachment"],
26754 isDefault: true,
26755 sort: 10,
26756 handler: {
26757 kind: "url",
26758 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
26759 }
26760 });
26761 registerOpener({
26762 id: "wp-user-profile",
26763 label: "User profile",
26764 types: ["user"],
26765 isDefault: true,
26766 sort: 10,
26767 handler: {
26768 kind: "url",
26769 url: (file) => `${adminBase()}user-edit.php?user_id=${encodeURIComponent(file.ref())}`
26770 }
26771 });
26772 registerOpener({
26773 id: "wp-term-editor",
26774 label: "Term editor",
26775 types: ["term"],
26776 isDefault: true,
26777 sort: 10,
26778 handler: {
26779 kind: "url",
26780 url: (file) => {
26781 const [taxonomy, termId] = file.ref().split(":");
26782 return `${adminBase()}term.php?taxonomy=${encodeURIComponent(taxonomy ?? "")}&tag_ID=${encodeURIComponent(termId ?? "")}`;
26783 }
26784 }
26785 });
26786 registerOpener({
26787 id: "wp-comment-editor",
26788 label: "Comment editor",
26789 types: ["comment"],
26790 isDefault: true,
26791 sort: 10,
26792 handler: {
26793 kind: "url",
26794 url: (file) => `${adminBase()}comment.php?action=editcomment&c=${encodeURIComponent(file.ref())}`
26795 }
26796 });
26797 registerOpener({
26798 id: "desktop-mode-upload-download",
26799 label: "Download",
26800 types: ["upload"],
26801 isDefault: true,
26802 sort: 10,
26803 handler: {
26804 kind: "js",
26805 open: (file) => {
26806 const fileId = parseInt(file.ref(), 10);
26807 if (!fileId) {
26808 return;
26809 }
26810 navigateToDownload(getUploadDownloadUrl(fileId));
26811 }
26812 }
26813 });
26814 registerOpener({
26815 id: "desktop-mode-folder-window",
26816 label: "Open folder",
26817 types: ["folder"],
26818 isDefault: true,
26819 sort: 10,
26820 handler: {
26821 kind: "js",
26822 open: (file) => {
26823 const folderId = parseInt(file.ref(), 10);
26824 if (!folderId) {
26825 return;
26826 }
26827 const wm = window.wp?.desktop?.windowManager;
26828 if (!wm) {
26829 return;
26830 }
26831 const id = `desktop-mode-folder-${folderId}`;
26832 const folderRow = store.getState().folders.get(folderId);
26833 const viewerId2 = Number(window.desktopModeConfig?.currentUserId ?? 0);
26834 const isRecipient = !!folderRow && folderRow.ownerId > 0 && folderRow.ownerId !== viewerId2;
26835 const baseTitle = file.title();
26836 const titleWithCue = isRecipient ? `${baseTitle} · Shared` : baseTitle;
26837 wm.open({
26838 id,
26839 baseId: id,
26840 url: `#folder-${folderId}`,
26841 title: titleWithCue,
26842 icon: file.icon(),
26843 native: true,
26844 render: (body) => {
26845 body.replaceChildren();
26846 body.classList.add("desktop-mode-folder-window");
26847 const routes = [
26848 { folderId, title: file.title() }
26849 ];
26850 let currentDispose = null;
26851 const breadcrumbsHost = document.createElement("header");
26852 body.appendChild(breadcrumbsHost);
26853 const bodyHost = document.createElement("div");
26854 bodyHost.style.cssText = "flex:1 1 auto;min-height:0;display:flex;flex-direction:column;";
26855 body.appendChild(bodyHost);
26856 const paintBreadcrumbs = () => {
26857 const segments = routes.map(
26858 (route, idx) => {
26859 const isCurrent = idx === routes.length - 1;
26860 if (isCurrent) {
26861 return { label: route.title };
26862 }
26863 return {
26864 label: route.title,
26865 onClick: () => {
26866 routes.length = idx + 1;
26867 mountCurrent();
26868 }
26869 };
26870 }
26871 );
26872 renderBreadcrumbs(breadcrumbsHost, segments, {
26873 onBack: () => {
26874 if (routes.length <= 1) {
26875 return;
26876 }
26877 routes.pop();
26878 mountCurrent();
26879 },
26880 backDisabled: routes.length <= 1
26881 });
26882 };
26883 const mountCurrent = () => {
26884 currentDispose?.();
26885 currentDispose = null;
26886 bodyHost.replaceChildren();
26887 const split = document.createElement("div");
26888 split.className = "desktop-mode-folder-window__split";
26889 bodyHost.appendChild(split);
26890 const layerHost = document.createElement("div");
26891 layerHost.className = "desktop-mode-folder-window__layer";
26892 split.appendChild(layerHost);
26893 const previewPane = document.createElement("div");
26894 previewPane.className = "desktop-mode-folder-window__preview";
26895 previewPane.appendChild(renderPreviewEmpty());
26896 split.appendChild(previewPane);
26897 const route = routes[routes.length - 1];
26898 const layer = mountFilesLayer(
26899 layerHost,
26900 route.folderId
26901 );
26902 const offSelection = layer.onSelectionChange(
26903 (placement) => {
26904 if (!placement) {
26905 previewPane.replaceChildren(
26906 renderPreviewEmpty()
26907 );
26908 return;
26909 }
26910 renderPlacementPreview(
26911 placement,
26912 previewPane
26913 );
26914 }
26915 );
26916 const dblClickHandler = (e) => {
26917 if (!(e.target instanceof Element)) {
26918 return;
26919 }
26920 const tile2 = e.target.closest(
26921 ".desktop-mode-file-tile"
26922 );
26923 if (!tile2) {
26924 return;
26925 }
26926 if (tile2.dataset.fileType !== "folder") {
26927 return;
26928 }
26929 const subId = parseInt(
26930 tile2.dataset.fileRef ?? "",
26931 10
26932 );
26933 if (!subId) {
26934 return;
26935 }
26936 e.preventDefault();
26937 e.stopPropagation();
26938 const subTitle = tile2.querySelector(
26939 ".desktop-mode-file-tile__label"
26940 )?.textContent ?? `#${subId}`;
26941 routes.push({
26942 folderId: subId,
26943 title: subTitle
26944 });
26945 mountCurrent();
26946 };
26947 layerHost.addEventListener(
26948 "dblclick",
26949 dblClickHandler,
26950 true
26951 );
26952 const menu = attachIconCanvasMenu(layerHost, {
26953 scope: `desktop-mode-folder:${route.folderId}`,
26954 onSort: (mode) => layer.sort(mode),
26955 extraItems: [
26956 {
26957 id: "new-folder",
26958 label: "New folder",
26959 icon: "dashicons-portfolio",
26960 sort: 5,
26961 onClick: () => {
26962 openCreateFolderDialog({
26963 onSubmit: async (name) => {
26964 const folder = await createFolder({
26965 name
26966 });
26967 const peers = store.getState().placementsByFolder.get(
26968 route.folderId
26969 ) ?? [];
26970 const occupied = buildOccupiedSet(peers);
26971 const cell = snapToEmptyCell(
26972 GRID_PADDING,
26973 GRID_PADDING,
26974 occupied,
26975 layerHost
26976 );
26977 const placement = await createPlacement({
26978 type: "folder",
26979 ref: String(folder.id),
26980 parentId: route.folderId,
26981 x: cell.x,
26982 y: cell.y
26983 });
26984 store.upsertFolder(folder);
26985 store.upsertPlacement(
26986 placement
26987 );
26988 }
26989 });
26990 }
26991 }
26992 ]
26993 });
26994 const status = mountFolderStatusBar(
26995 bodyHost,
26996 route.folderId
26997 );
26998 currentDispose = () => {
26999 offSelection();
27000 menu.dispose();
27001 status.dispose();
27002 layerHost.removeEventListener(
27003 "dblclick",
27004 dblClickHandler,
27005 true
27006 );
27007 layer.dispose();
27008 };
27009 paintBreadcrumbs();
27010 };
27011 mountCurrent();
27012 },
27013 width: 720,
27014 height: 480,
27015 minWidth: 360,
27016 minHeight: 240
27017 });
27018 }
27019 }
27020 });
27021 registerOpener({
27022 id: "desktop-mode-shortcut-opener",
27023 label: "Open shortcut",
27024 types: ["shortcut"],
27025 isDefault: true,
27026 sort: 10,
27027 handler: {
27028 kind: "js",
27029 open: (file) => {
27030 const extras = file.shape;
27031 const wp = window.wp?.desktop;
27032 if (!wp) {
27033 return;
27034 }
27035 if (extras.shortcutWindow && wp.openWindow) {
27036 wp.openWindow(extras.shortcutWindow);
27037 return;
27038 }
27039 if (extras.shortcutUrl && wp.windowManager) {
27040 try {
27041 const u = new URL(extras.shortcutUrl, window.location.origin);
27042 if (u.origin !== window.location.origin) {
27043 window.open(u.toString(), "_blank", "noopener,noreferrer");
27044 return;
27045 }
27046 const adminUrl = wp.config?.adminUrl;
27047 const id = adminUrl ? deriveWindowId(u.toString(), adminUrl) : `desktop-icon-${file.ref()}`;
27048 const entry = findMenuEntryForUrl(u.toString());
27049 wp.windowManager.open({
27050 id,
27051 baseId: id,
27052 url: u.toString(),
27053 parentUrl: entry?.url ?? u.toString(),
27054 title: file.title(),
27055 icon: file.icon(),
27056 submenu: entry?.submenu,
27057 multi: !!entry?.multi
27058 });
27059 } catch {
27060 }
27061 }
27062 }
27063 }
27064 });
27065 registerOpener({
27066 id: "browser-navigate",
27067 label: "Open in browser",
27068 types: ["bookmark"],
27069 isDefault: true,
27070 sort: 10,
27071 handler: {
27072 kind: "js",
27073 open: (file) => {
27074 const url = sanitizedWebUrl(file);
27075 if (!url) {
27076 return;
27077 }
27078 window.open(url, "_blank", "noopener,noreferrer");
27079 }
27080 }
27081 });
27082 registerOpener({
27083 id: "desktop-mode-link-opener",
27084 label: "Open in browser",
27085 types: ["link"],
27086 isDefault: true,
27087 sort: 10,
27088 handler: {
27089 kind: "js",
27090 open: (file) => {
27091 const url = sanitizedWebUrl(file);
27092 if (!url) {
27093 return;
27094 }
27095 window.open(url, "_blank", "noopener,noreferrer");
27096 }
27097 }
27098 });
27099 registerOpener({
27100 id: "desktop-mode-embed-opener",
27101 label: "Open as window",
27102 types: ["embed"],
27103 isDefault: true,
27104 sort: 10,
27105 handler: {
27106 kind: "js",
27107 open: (file, ctx) => {
27108 openEmbedWindow(file, ctx);
27109 }
27110 }
27111 });
27112 }
27113 const TAB_ID = "desktop-mode-file-associations";
27114 function registerFileAssociationsTab() {
27115 registerSettingsTab({
27116 id: TAB_ID,
27117 label: "File Associations",
27118 order: 50,
27119 render(body) {
27120 renderTab(body);
27121 }
27122 });
27123 }
27124 function renderTab(body) {
27125 body.replaceChildren();
27126 const types = getTypes();
27127 if (types.length === 0) {
27128 const empty = document.createElement("p");
27129 empty.className = "desktop-mode-file-associations__empty";
27130 empty.textContent = "No file types are registered.";
27131 body.appendChild(empty);
27132 return;
27133 }
27134 const intro = document.createElement("p");
27135 intro.className = "desktop-mode-file-associations__intro";
27136 intro.textContent = "Pick which app opens each kind of file when you double-click it on the desktop.";
27137 body.appendChild(intro);
27138 const associations = getUserAssociations();
27139 const list2 = document.createElement("div");
27140 list2.className = "desktop-mode-file-associations__list";
27141 list2.setAttribute("role", "list");
27142 for (const type of types) {
27143 list2.appendChild(buildRow(type.type, type.label, associations));
27144 }
27145 body.appendChild(list2);
27146 }
27147 function buildRow(typeSlug, typeLabel, associations) {
27148 const row = document.createElement("div");
27149 row.className = "desktop-mode-file-associations__row";
27150 row.setAttribute("role", "listitem");
27151 row.dataset.fileType = typeSlug;
27152 const label = document.createElement("label");
27153 label.className = "desktop-mode-file-associations__label";
27154 label.textContent = typeLabel;
27155 row.appendChild(label);
27156 const candidates = getOpenersForType(typeSlug);
27157 if (candidates.length === 0) {
27158 const empty = document.createElement("span");
27159 empty.className = "desktop-mode-file-associations__none";
27160 empty.textContent = "No app available";
27161 row.appendChild(empty);
27162 return row;
27163 }
27164 const resolved = resolveOpener(typeSlug);
27165 const currentId = associations[typeSlug] ?? resolved?.id ?? "";
27166 const select = document.createElement("wpd-select");
27167 select.setAttribute("value", currentId);
27168 select.setAttribute("aria-label", `Default app for ${typeLabel}`);
27169 select.className = "desktop-mode-file-associations__select";
27170 label.htmlFor = `assoc-${typeSlug}`;
27171 select.id = `assoc-${typeSlug}`;
27172 for (const o of candidates) {
27173 const opt = document.createElement("wpd-option");
27174 opt.setAttribute("value", o.id);
27175 opt.textContent = o.isDefault ? `${o.label} (default)` : o.label;
27176 select.appendChild(opt);
27177 }
27178 select.addEventListener("wpd-pick", (e) => {
27179 const next = e.detail?.value;
27180 if (!next) {
27181 return;
27182 }
27183 const merged = { ...getUserAssociations(), [typeSlug]: next };
27184 setUserAssociations(merged);
27185 void saveAssociations(merged).catch((err) => {
27186 console.error("[desktop-mode] saveAssociations failed:", err);
27187 });
27188 });
27189 row.appendChild(select);
27190 return row;
27191 }
27192 let _store$1 = null;
27193 function sharesStore() {
27194 if (!_store$1) {
27195 _store$1 = createSharedStore("desktop-files/shares", () => ({
27196 byFolder: /* @__PURE__ */ new Map(),
27197 pending: [],
27198 sharesVersion: 0,
27199 deniedFolders: /* @__PURE__ */ new Set(),
27200 deniedFiles: /* @__PURE__ */ new Set()
27201 }));
27202 }
27203 return _store$1;
27204 }
27205 function setSharesForFolder(folderId, shares) {
27206 const s = sharesStore();
27207 s.state.byFolder.set(folderId, shares);
27208 s.notify();
27209 }
27210 function upsertShare(share) {
27211 if (!share || typeof share.folderId !== "number") {
27212 return;
27213 }
27214 const s = sharesStore();
27215 const existing = s.state.byFolder.get(share.folderId) ?? [];
27216 const next = existing.filter((r) => r.id !== share.id);
27217 next.push(share);
27218 s.state.byFolder.set(share.folderId, next);
27219 s.notify();
27220 }
27221 function removeShare(folderId, shareId) {
27222 const s = sharesStore();
27223 const existing = s.state.byFolder.get(folderId) ?? [];
27224 s.state.byFolder.set(
27225 folderId,
27226 existing.filter((r) => r.id !== shareId)
27227 );
27228 s.notify();
27229 }
27230 function inviteEquals(a, b) {
27231 return a.id === b.id && a.folderId === b.folderId && a.capability === b.capability && a.invitedAtMs === b.invitedAtMs && a.folderName === b.folderName && a.fileName === b.fileName && a.ownerName === b.ownerName;
27232 }
27233 function ingestPendingInvites(invites) {
27234 const s = sharesStore();
27235 const existingById = new Map(s.state.pending.map((p) => [p.id, p]));
27236 let mutated = false;
27237 for (const raw of invites) {
27238 const inv = raw.targetType === "file" && typeof raw.folderId !== "number" ? { ...raw, folderId: 0 } : raw;
27239 if (inv.targetType === "file") {
27240 if (typeof inv.fileId === "number" && s.state.deniedFiles.has(inv.fileId)) {
27241 continue;
27242 }
27243 } else if (s.state.deniedFolders.has(inv.folderId)) {
27244 continue;
27245 }
27246 const existing = existingById.get(inv.id);
27247 if (existing) {
27248 if (inviteEquals(existing, inv)) {
27249 continue;
27250 }
27251 s.state.pending = s.state.pending.map((p) => p.id === inv.id ? inv : p);
27252 } else {
27253 s.state.pending.push(inv);
27254 }
27255 if (inv.invitedAtMs > s.state.sharesVersion) {
27256 s.state.sharesVersion = inv.invitedAtMs;
27257 }
27258 mutated = true;
27259 }
27260 if (mutated) {
27261 s.notify();
27262 }
27263 }
27264 function dropPending(shareId, opts = {}) {
27265 const s = sharesStore();
27266 s.state.pending = s.state.pending.filter((p) => p.id !== shareId);
27267 if (opts.denied && typeof opts.folderId === "number") {
27268 s.state.deniedFolders.add(opts.folderId);
27269 }
27270 if (opts.denied && typeof opts.fileId === "number") {
27271 s.state.deniedFiles.add(opts.fileId);
27272 }
27273 s.notify();
27274 }
27275 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}`;
27276 const _WpdUserSearch = class _WpdUserSearch extends Component {
27277 constructor() {
27278 super(...arguments);
27279 this._timer = null;
27280 this._abort = null;
27281 this._results = [];
27282 this._query = "";
27283 this._open = false;
27284 this._phase = "idle";
27285 this._error = "";
27286 this._dropdownStyle = "";
27287 this._onScrollOrResize = () => void 0;
27288 this._onInput = (e) => {
27289 const value = e.target.value;
27290 this._query = value;
27291 this._scheduleSearch(value);
27292 };
27293 this._onFocus = () => {
27294 if (this._results.length === 0 && this._phase === "idle") {
27295 this._scheduleSearch(this._query);
27296 return;
27297 }
27298 this._open = true;
27299 this._positionDropdown();
27300 this.requestUpdate();
27301 };
27302 this._onBlur = () => {
27303 setTimeout(() => {
27304 this._open = false;
27305 this.requestUpdate();
27306 }, 150);
27307 };
27308 this._pick = (user) => {
27309 this.emit("wpd-user-pick", { user });
27310 this._results = [];
27311 this._open = false;
27312 this._phase = "idle";
27313 this._query = "";
27314 const input = this.shadowRoot?.querySelector(".input");
27315 if (input) {
27316 input.value = "";
27317 }
27318 this.requestUpdate();
27319 };
27320 }
27321 connectedCallback() {
27322 super.connectedCallback();
27323 this._onScrollOrResize = () => {
27324 if (this._open) {
27325 this._positionDropdown();
27326 this.requestUpdate();
27327 }
27328 };
27329 window.addEventListener("resize", this._onScrollOrResize);
27330 window.addEventListener("scroll", this._onScrollOrResize, true);
27331 }
27332 disconnectedCallback() {
27333 if (this._timer) {
27334 clearTimeout(this._timer);
27335 }
27336 if (this._abort) {
27337 this._abort.abort();
27338 }
27339 window.removeEventListener("resize", this._onScrollOrResize);
27340 window.removeEventListener("scroll", this._onScrollOrResize, true);
27341 }
27342 _endpoint() {
27343 const attr = this.getAttribute("endpoint");
27344 if (attr) {
27345 return attr;
27346 }
27347 return window.desktopModeConfig?.filesUsersSearchUrl || "";
27348 }
27349 _scheduleSearch(q) {
27350 if (this._timer) {
27351 clearTimeout(this._timer);
27352 }
27353 this._phase = "loading";
27354 this._open = true;
27355 this._positionDropdown();
27356 this.requestUpdate();
27357 this._timer = setTimeout(() => this._runSearch(q), 200);
27358 }
27359 async _runSearch(q) {
27360 const url = this._endpoint();
27361 if (!url) {
27362 this._phase = "error";
27363 this._error = "Search endpoint is not configured.";
27364 this._results = [];
27365 this._open = true;
27366 this.requestUpdate();
27367 return;
27368 }
27369 if (this._abort) {
27370 this._abort.abort();
27371 }
27372 const ctrl = new AbortController();
27373 this._abort = ctrl;
27374 const exclude = this.getAttribute("exclude") || "";
27375 const full = url + "?q=" + encodeURIComponent(q) + "&exclude=" + encodeURIComponent(exclude);
27376 try {
27377 const init2 = {
27378 signal: ctrl.signal,
27379 credentials: "same-origin"
27380 };
27381 const res = await trackedFetch$1(full, init2, {
27382 source: "desktop-mode/files-user-search",
27383 silent: true
27384 });
27385 if (!res.ok) {
27386 throw new Error(`HTTP ${res.status}`);
27387 }
27388 const json = await res.json();
27389 this._results = json && Array.isArray(json.users) ? json.users : [];
27390 this._phase = "ready";
27391 this._error = "";
27392 this._open = true;
27393 } catch (e) {
27394 if (e.name === "AbortError") {
27395 return;
27396 }
27397 this._results = [];
27398 this._phase = "error";
27399 this._error = e.message || "Search failed.";
27400 this._open = true;
27401 }
27402 this._positionDropdown();
27403 this.requestUpdate();
27404 }
27405 _positionDropdown() {
27406 const input = this.shadowRoot?.querySelector(".input");
27407 if (!input) {
27408 return;
27409 }
27410 const rect = input.getBoundingClientRect();
27411 const top = rect.bottom + 4;
27412 const left = rect.left;
27413 const width = rect.width;
27414 const viewportH = window.innerHeight;
27415 const spaceBelow = viewportH - rect.bottom;
27416 const spaceAbove = rect.top;
27417 const maxHeight = Math.max(120, Math.min(280, Math.max(spaceBelow, spaceAbove) - 16));
27418 if (spaceBelow < 200 && spaceAbove > spaceBelow) {
27419 this._dropdownStyle = [
27420 "position:fixed",
27421 `left:${left}px`,
27422 `top:${rect.top - 4 - maxHeight}px`,
27423 `width:${width}px`,
27424 `max-height:${maxHeight}px`
27425 ].join(";");
27426 } else {
27427 this._dropdownStyle = [
27428 "position:fixed",
27429 `left:${left}px`,
27430 `top:${top}px`,
27431 `width:${width}px`,
27432 `max-height:${maxHeight}px`
27433 ].join(";");
27434 }
27435 }
27436 _dropdownContent() {
27437 if (this._phase === "loading") {
27438 return html`<div class="empty">Searching…</div>`;
27439 }
27440 if (this._phase === "error") {
27441 return html`<div class="empty error">${this._error}</div>`;
27442 }
27443 if (this._results.length === 0) {
27444 const message = this._query ? "No matches." : "No users available.";
27445 return html`<div class="empty">${message}</div>`;
27446 }
27447 return this._results.map(
27448 (u) => html`
27449 <button
27450 type="button"
27451 class="item"
27452 role="option"
27453 @mousedown=${(e) => e.preventDefault()}
27454 @click=${() => this._pick(u)}
27455 >
27456 <img class="avatar" src=${u.avatarUrl} alt="" />
27457 <div>
27458 <div class="name">${u.name}</div>
27459 <div class="slug">${u.slug}</div>
27460 </div>
27461 </button>
27462 `
27463 );
27464 }
27465 render() {
27466 const placeholder = this.getAttribute("placeholder") || "Search users…";
27467 return html`
27468 <input
27469 class="input"
27470 type="search"
27471 placeholder=${placeholder}
27472 autocomplete="off"
27473 @input=${this._onInput}
27474 @focus=${this._onFocus}
27475 @blur=${this._onBlur}
27476 .value=${this._query}
27477 />
27478 ${this._open ? html`
27479 <div class="dropdown" role="listbox" style=${this._dropdownStyle}>
27480 ${this._dropdownContent()}
27481 </div>
27482 ` : html``}
27483 `;
27484 }
27485 };
27486 _WpdUserSearch.props = ["placeholder", "exclude", "endpoint"];
27487 _WpdUserSearch.styles = [userSearchStyles];
27488 _WpdUserSearch.help = {
27489 title: "User autocomplete",
27490 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.",
27491 status: "experimental",
27492 since: "0.8.5",
27493 props: [
27494 { name: "placeholder", type: "string", description: "Input placeholder text." },
27495 {
27496 name: "exclude",
27497 type: "csv user ids",
27498 description: "Already-picked user ids to suppress in results."
27499 },
27500 {
27501 name: "endpoint",
27502 type: "URL",
27503 description: "Override the search URL (defaults to desktopModeConfig.filesUsersSearchUrl)."
27504 }
27505 ],
27506 events: [
27507 { name: "wpd-user-pick", description: "Emitted on pick. Detail: `{ user: SearchUser }`." }
27508 ]
27509 };
27510 let WpdUserSearch = _WpdUserSearch;
27511 defineComponent("wpd-user-search", WpdUserSearch);
27512 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}`;
27513 const _WpdRolePicker = class _WpdRolePicker extends Component {
27514 constructor() {
27515 super(...arguments);
27516 this._onToggle = (slug) => {
27517 const selected = !this._selectedSet().has(slug);
27518 this.emit("wpd-role-toggle", { slug, selected });
27519 };
27520 }
27521 _selectedSet() {
27522 const raw = this.getAttribute("selected") || "";
27523 return new Set(
27524 raw.split(",").map((s) => s.trim()).filter((s) => s !== "")
27525 );
27526 }
27527 _roles() {
27528 const attr = this.getAttribute("roles");
27529 if (attr) {
27530 try {
27531 const parsed = JSON.parse(attr);
27532 if (Array.isArray(parsed)) {
27533 return parsed;
27534 }
27535 } catch (e) {
27536 }
27537 }
27538 return window.desktopModeConfig?.shareEligibleRoles || [];
27539 }
27540 render() {
27541 const roles = this._roles();
27542 if (roles.length === 0) {
27543 return html`<span class="empty">No eligible roles.</span>`;
27544 }
27545 const set = this._selectedSet();
27546 return html`
27547 ${roles.map((r) => {
27548 const isSelected = set.has(r.slug);
27549 return html`
27550 <button
27551 type="button"
27552 class="chip"
27553 aria-pressed=${isSelected ? "true" : "false"}
27554 @click=${() => this._onToggle(r.slug)}
27555 >${r.name}</button>
27556 `;
27557 })}
27558 `;
27559 }
27560 };
27561 _WpdRolePicker.props = ["selected", "roles"];
27562 _WpdRolePicker.styles = [rolePickerStyles];
27563 _WpdRolePicker.help = {
27564 title: "Role picker",
27565 summary: "Chip multi-select for WordPress roles. Reads eligible roles from desktopModeConfig.shareEligibleRoles; emits wpd-role-toggle { slug, selected } on every change.",
27566 status: "experimental",
27567 since: "0.8.5",
27568 props: [
27569 {
27570 name: "selected",
27571 type: "csv role slugs",
27572 description: "Comma-separated role slugs that are currently selected."
27573 },
27574 {
27575 name: "roles",
27576 type: "JSON",
27577 description: "Override the source of eligible roles (defaults to the global config)."
27578 }
27579 ],
27580 events: [
27581 {
27582 name: "wpd-role-toggle",
27583 description: "Emitted on every click. Detail: `{ slug, selected }`."
27584 }
27585 ]
27586 };
27587 let WpdRolePicker = _WpdRolePicker;
27588 defineComponent("wpd-role-picker", WpdRolePicker);
27589 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}`;
27590 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}`;
27591 const _WpdSegment = class _WpdSegment extends Component {
27592 render() {
27593 this.setAttribute("role", "radio");
27594 return html`
27595 <button type="button" @click=${() => this._onPick()}>
27596 <slot></slot>
27597 </button>
27598 `;
27599 }
27600 _onPick() {
27601 this.emit("wpd-segment-pick", {
27602 value: this.value
27603 });
27604 }
27605 };
27606 _WpdSegment.props = ["value"];
27607 _WpdSegment.styles = [segmentStyles];
27608 _WpdSegment.help = {
27609 title: "Segment",
27610 summary: "Single pill inside a <wpd-segmented> group. Value identifies it for selection; aria-checked is mirrored by the parent.",
27611 status: "stable",
27612 since: "0.9.0",
27613 props: [
27614 {
27615 name: "value",
27616 type: "string",
27617 description: "Identifier this segment contributes to the parent group selection."
27618 }
27619 ],
27620 slots: [
27621 { name: "(default)", description: "Visible segment label." }
27622 ],
27623 events: [
27624 {
27625 name: "wpd-segment-pick",
27626 description: "Internal event bubbled to the parent <wpd-segmented>. Consumers should listen for wpd-pick on the group instead.",
27627 detail: "{ value: string }"
27628 }
27629 ]
27630 };
27631 let WpdSegment = _WpdSegment;
27632 defineComponent("wpd-segment", WpdSegment);
27633 const _WpdSegmented = class _WpdSegmented extends Component {
27634 connectedCallback() {
27635 super.connectedCallback();
27636 this.addEventListener("wpd-segment-pick", (e) => {
27637 const detail = e.detail;
27638 e.stopPropagation();
27639 this.value = detail.value;
27640 this.emit("wpd-pick", { value: detail.value });
27641 });
27642 }
27643 /**
27644 * Declarative item-list setter. Replaces the existing
27645 * `<wpd-segment>` children with a fresh set built from a
27646 * `{ value, label }` array; preserves the current selection
27647 * when the value still matches an entry, otherwise falls back
27648 * to the first item.
27649 *
27650 * Collapses the pre-0.11 imperative dance (clear children,
27651 * `createElement`, set `textContent`, `appendChild`, then
27652 * `setAttribute('value', …)` on the group — order matters) to
27653 * a single assignment:
27654 *
27655 * ```js
27656 * segmented.items = [
27657 * { value: 'm', label: 'm' },
27658 * { value: 'km', label: 'km' },
27659 * ];
27660 * ```
27661 *
27662 * @since 0.5.0
27663 */
27664 set items(list2) {
27665 const existing = this.querySelectorAll(":scope > wpd-segment");
27666 for (const el of Array.from(existing)) {
27667 el.remove();
27668 }
27669 for (const item of list2) {
27670 const seg = document.createElement("wpd-segment");
27671 seg.setAttribute("value", item.value);
27672 seg.textContent = item.label;
27673 this.appendChild(seg);
27674 }
27675 const current = this.value;
27676 const stillValid = current !== null && list2.some((i) => i.value === current);
27677 if (!stillValid && list2.length > 0) {
27678 this.value = list2[0].value;
27679 } else {
27680 this.requestUpdate();
27681 }
27682 }
27683 render() {
27684 const label = this.label || "";
27685 if (label) {
27686 this.setAttribute("aria-label", label);
27687 }
27688 this.setAttribute("role", "radiogroup");
27689 const current = this.value;
27690 queueMicrotask(() => {
27691 const segs = this.querySelectorAll("wpd-segment");
27692 for (const seg of Array.from(segs)) {
27693 const v = seg.getAttribute("value");
27694 seg.setAttribute(
27695 "aria-checked",
27696 v === current ? "true" : "false"
27697 );
27698 }
27699 });
27700 return html`<slot></slot>`;
27701 }
27702 };
27703 _WpdSegmented.props = ["value", "label"];
27704 _WpdSegmented.styles = [segmentedStyles];
27705 _WpdSegmented.help = {
27706 title: "Segmented",
27707 summary: "iOS-style segmented radio group. Pill-shaped bar of equal-width <wpd-segment> children where exactly one is active.",
27708 status: "stable",
27709 since: "0.9.0",
27710 props: [
27711 {
27712 name: "value",
27713 type: "string",
27714 description: "Currently selected segment value. Mirrored onto child aria-checked."
27715 },
27716 {
27717 name: "label",
27718 type: "string",
27719 description: "aria-label for the radiogroup."
27720 }
27721 ],
27722 slots: [
27723 { name: "(default)", description: '<wpd-segment value="…"> children.' }
27724 ],
27725 events: [
27726 {
27727 name: "wpd-pick",
27728 description: "Fires when the selected segment changes.",
27729 detail: "{ value: string }"
27730 }
27731 ],
27732 cssProps: [
27733 { name: "--desktop-mode-window-bg", description: "Pill background." },
27734 { name: "--desktop-mode-text", description: "Active label colour." },
27735 { name: "--desktop-mode-muted", description: "Inactive label colour." }
27736 ],
27737 example: html`
27738 <wpd-segmented value="md" label="Dock size">
27739 <wpd-segment value="sm">Small</wpd-segment>
27740 <wpd-segment value="md">Medium</wpd-segment>
27741 <wpd-segment value="lg">Large</wpd-segment>
27742 </wpd-segmented>
27743 `
27744 };
27745 let WpdSegmented = _WpdSegmented;
27746 defineComponent("wpd-segmented", WpdSegmented);
27747 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}`;
27748 const toastStyles = css`:host{display:flex;align-items:center;gap:12px;min-width:280px;max-width:420px;padding:10px 14px;background:#1d2327;color:#fff;border-radius:10px;border:1px solid rgba( 255,255,255,0.12 );box-shadow:0 10px 30px rgba( 0,0,0,0.4 ),0 2px 6px rgba( 0,0,0,0.18 ),inset 0 0 0 1px rgba( 255,255,255,0.04 );font-size:13px;line-height:1.4;opacity:0;transform:translateY( -8px );transition:opacity 0.18s ease,transform 0.18s ease;pointer-events:auto}:host( [ state='in' ] ){opacity:1;transform:translateY( 0 )}:host( [ state='out' ] ){opacity:0;transform:translateY( -8px )}.wpd-toast__label{flex:1}button[ hidden ]{display:none}button{flex-shrink:0;padding:4px 10px;border:none;border-radius:4px;background:rgba( 255,255,255,0.12 );color:#fff;font:inherit;font-size:12px;font-weight:500;cursor:pointer;transition:background-color 0.12s ease}button:hover{background:rgba( 255,255,255,0.22 )}button:focus-visible{outline:2px solid rgba( 255,255,255,0.6 );outline-offset:2px}.wpd-toast__close{display:inline-flex;align-items:center;justify-content:center;padding:4px;border-radius:6px;background:transparent;color:rgba( 255,255,255,0.7 )}.wpd-toast__close:hover{background:rgba( 255,255,255,0.14 );color:#fff}@media ( prefers-reduced-motion:reduce ){:host{transition-duration:0.01ms}}`;
27749 const _WpdToastContainer = class _WpdToastContainer extends Component {
27750 connectedCallback() {
27751 super.connectedCallback();
27752 this.setAttribute("aria-live", "polite");
27753 }
27754 render() {
27755 return html`<slot></slot>`;
27756 }
27757 };
27758 _WpdToastContainer.styles = [containerStyles];
27759 _WpdToastContainer.help = {
27760 title: "Toast container",
27761 summary: "Singleton stack beneath <body> that hosts transient <wpd-toast> notifications in the top-right. Created lazily by showToast(); authors rarely place one themselves.",
27762 status: "stable",
27763 since: "0.9.0",
27764 slots: [
27765 { name: "(default)", description: "<wpd-toast> children, stacked vertically." }
27766 ],
27767 cssProps: [
27768 { name: "--desktop-mode-z-fullscreen", description: "z-index base — toasts sit above fullscreen windows." }
27769 ],
27770 example: html`
27771 <wpd-toast-container>
27772 <wpd-toast state="in">Settings saved.</wpd-toast>
27773 <wpd-toast state="in" action="Undo">Theme changed.</wpd-toast>
27774 </wpd-toast-container>
27775 `
27776 };
27777 let WpdToastContainer = _WpdToastContainer;
27778 defineComponent("wpd-toast-container", WpdToastContainer);
27779 const _WpdToast = class _WpdToast extends Component {
27780 connectedCallback() {
27781 super.connectedCallback();
27782 if (!this.hasAttribute("role")) {
27783 this.setAttribute("role", "status");
27784 }
27785 }
27786 render() {
27787 const action = this.action || "";
27788 const dismissible = this.hasAttribute("dismissible");
27789 return html`
27790 <span class="wpd-toast__label"><slot></slot></span>
27791 <button
27792 type="button"
27793 ?hidden=${!action}
27794 @click=${(e) => this._onAction(e)}
27795 >
27796 ${action}
27797 </button>
27798 <button
27799 type="button"
27800 class="wpd-toast__close"
27801 aria-label=${__("Dismiss")}
27802 ?hidden=${!dismissible}
27803 @click=${(e) => this._onDismiss(e)}
27804 >
27805 <svg viewBox="0 0 14 14" width="12" height="12" aria-hidden="true" focusable="false">
27806 <path
27807 d="M3 3 L11 11 M11 3 L3 11"
27808 stroke="currentColor"
27809 stroke-width="1.7"
27810 stroke-linecap="round"
27811 fill="none"
27812 ></path>
27813 </svg>
27814 </button>
27815 `;
27816 }
27817 _onAction(e) {
27818 e.preventDefault();
27819 e.stopPropagation();
27820 this.emit("wpd-toast-action", {});
27821 }
27822 _onDismiss(e) {
27823 e.preventDefault();
27824 e.stopPropagation();
27825 this.emit("wpd-toast-dismiss", {});
27826 }
27827 };
27828 _WpdToast.props = ["action", "state", "dismissible"];
27829 _WpdToast.styles = [toastStyles];
27830 _WpdToast.help = {
27831 title: "Toast",
27832 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.',
27833 status: "stable",
27834 since: "0.9.0",
27835 props: [
27836 {
27837 name: "action",
27838 type: "string",
27839 description: "Optional action button label. When set, a button renders on the right and emits wpd-toast-action on click."
27840 },
27841 {
27842 name: "state",
27843 type: "'in' | 'out'",
27844 description: 'Drives the CSS fade transition. Set to "in" when rendered, flip to "out" before removal.'
27845 },
27846 {
27847 name: "dismissible",
27848 type: "boolean",
27849 description: "When set, a close (×) button renders on the right and emits wpd-toast-dismiss on click. Use for persistent toasts the user must be able to close."
27850 }
27851 ],
27852 slots: [
27853 { name: "(default)", description: "Message text." }
27854 ],
27855 events: [
27856 {
27857 name: "wpd-toast-action",
27858 description: "Fires when the action button is clicked.",
27859 detail: "{}"
27860 },
27861 {
27862 name: "wpd-toast-dismiss",
27863 description: "Fires when the close (×) button is clicked.",
27864 detail: "{}"
27865 }
27866 ],
27867 example: html`
27868 <wpd-toast state="in" action="Undo">Post moved to trash.</wpd-toast>
27869 `
27870 };
27871 let WpdToast = _WpdToast;
27872 defineComponent("wpd-toast", WpdToast);
27873 function buildCapSegmented(initial, onChange) {
27874 const segmented = document.createElement("wpd-segmented");
27875 segmented.setAttribute("value", initial);
27876 segmented.setAttribute("label", "Capability");
27877 segmented.style.setProperty("--wpd-segmented-bg", "rgba(255,255,255,0.06)");
27878 segmented.style.setProperty(
27879 "--desktop-mode-window-bg",
27880 "var(--wp-admin-theme-color, #2271b1)"
27881 );
27882 segmented.style.setProperty("--desktop-mode-text", "#fff");
27883 segmented.style.setProperty("--desktop-mode-muted", "rgba(255,255,255,0.65)");
27884 const segRead = document.createElement("wpd-segment");
27885 segRead.setAttribute("value", "read");
27886 segRead.textContent = "Read";
27887 segmented.appendChild(segRead);
27888 const segWrite = document.createElement("wpd-segment");
27889 segWrite.setAttribute("value", "write");
27890 segWrite.textContent = "Read + Write";
27891 segmented.appendChild(segWrite);
27892 segmented.addEventListener("wpd-pick", (e) => {
27893 const detail = e.detail;
27894 onChange(detail.value);
27895 });
27896 return segmented;
27897 }
27898 function buildIconButton(label, onClick, opts = {}) {
27899 const btn = document.createElement("wpd-button");
27900 btn.setAttribute("variant", "ghost");
27901 btn.setAttribute("aria-label", opts.danger ? "Remove" : "Dismiss");
27902 btn.textContent = label;
27903 const fg = opts.danger ? "#ff8080" : "rgba(255,255,255,0.75)";
27904 const border = opts.danger ? "1px solid rgba(255,128,128,0.45)" : "1px solid rgba(255,255,255,0.18)";
27905 btn.style.setProperty("--wpd-button-fg", fg);
27906 btn.style.setProperty("--wpd-button-border", border);
27907 btn.style.setProperty("--wpd-button-padding", "6px 12px");
27908 btn.style.setProperty("--wpd-button-border-radius", "7px");
27909 btn.style.setProperty("--wpd-button-min-height", "34px");
27910 btn.style.minWidth = "34px";
27911 btn.style.fontSize = "18px";
27912 btn.style.lineHeight = "1";
27913 btn.addEventListener("click", onClick);
27914 return btn;
27915 }
27916 async function openShareSettingsModal(opts) {
27917 const modal = document.createElement("wpd-modal");
27918 modal.setAttribute("open", "");
27919 modal.setAttribute("size", "lg");
27920 modal.setAttribute("title", `Share "${opts.folderName}"`);
27921 document.body.appendChild(modal);
27922 let shares = [];
27923 let pendingPicks = [];
27924 const renderBody = () => {
27925 modal.innerHTML = "";
27926 const owner = document.createElement("div");
27927 owner.style.cssText = "opacity:0.7;margin-bottom:14px;font-size:12px;";
27928 owner.textContent = opts.ownerName ? `Owner: ${opts.ownerName} — cannot be changed` : "Owner cannot be changed";
27929 modal.appendChild(owner);
27930 const addPeople = document.createElement("div");
27931 addPeople.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
27932 const addPeopleLabel = document.createElement("div");
27933 addPeopleLabel.textContent = "Add people";
27934 addPeopleLabel.style.cssText = "font-weight:600;";
27935 addPeople.appendChild(addPeopleLabel);
27936 const userSearch = document.createElement("wpd-user-search");
27937 const excludedUserIds = shares.filter((s) => s.principalType === "user").map((s) => s.principalRef).concat(pendingPicks.filter((p) => p.kind === "user").map((p) => p.ref));
27938 userSearch.setAttribute("exclude", excludedUserIds.join(","));
27939 userSearch.setAttribute("placeholder", "Search users…");
27940 userSearch.addEventListener("wpd-user-pick", (e) => {
27941 const detail = e.detail;
27942 pendingPicks.push({
27943 kind: "user",
27944 ref: String(detail.user.id),
27945 label: detail.user.name,
27946 cap: "read"
27947 });
27948 renderBody();
27949 });
27950 addPeople.appendChild(userSearch);
27951 modal.appendChild(addPeople);
27952 const addRoles = document.createElement("div");
27953 addRoles.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
27954 const addRolesLabel = document.createElement("div");
27955 addRolesLabel.textContent = "Add roles";
27956 addRolesLabel.style.cssText = "font-weight:600;";
27957 addRoles.appendChild(addRolesLabel);
27958 const rolePicker = document.createElement("wpd-role-picker");
27959 const grantedRoles = shares.filter((s) => s.principalType === "role").map((s) => s.principalRef);
27960 const pickedRoles = pendingPicks.filter((p) => p.kind === "role").map((p) => p.ref);
27961 rolePicker.setAttribute("selected", [...grantedRoles, ...pickedRoles].join(","));
27962 rolePicker.addEventListener("wpd-role-toggle", (e) => {
27963 const detail = e.detail;
27964 const existing = shares.find(
27965 (s) => s.principalType === "role" && s.principalRef === detail.slug
27966 );
27967 if (existing) {
27968 if (!detail.selected) {
27969 void revoke(existing);
27970 }
27971 return;
27972 }
27973 if (detail.selected) {
27974 const eligible = (window.desktopModeConfig?.shareEligibleRoles ?? []).find(
27975 (r) => r.slug === detail.slug
27976 );
27977 pendingPicks.push({
27978 kind: "role",
27979 ref: detail.slug,
27980 label: eligible ? eligible.name : detail.slug,
27981 cap: "read"
27982 });
27983 } else {
27984 pendingPicks = pendingPicks.filter(
27985 (p) => !(p.kind === "role" && p.ref === detail.slug)
27986 );
27987 }
27988 renderBody();
27989 });
27990 addRoles.appendChild(rolePicker);
27991 modal.appendChild(addRoles);
27992 if (pendingPicks.length > 0) {
27993 const pendingBlock = document.createElement("div");
27994 pendingBlock.style.cssText = "border:1px dashed rgba(255,255,255,0.18);border-radius:8px;padding:10px;margin-bottom:14px;";
27995 const pendingTitle = document.createElement("div");
27996 pendingTitle.textContent = "New invites (not sent yet)";
27997 pendingTitle.style.cssText = "font-weight:600;margin-bottom:6px;font-size:12px;";
27998 pendingBlock.appendChild(pendingTitle);
27999 for (const pick of pendingPicks) {
28000 const row = document.createElement("div");
28001 row.style.cssText = "display:flex;align-items:center;gap:8px;padding:4px 0;font-size:13px;";
28002 const tag = document.createElement("span");
28003 tag.textContent = pick.kind === "role" ? `Role: ${pick.label}` : pick.label;
28004 tag.style.flex = "1";
28005 row.appendChild(tag);
28006 const capSeg = buildCapSegmented(pick.cap, (next) => {
28007 pick.cap = next;
28008 });
28009 row.appendChild(capSeg);
28010 const removeBtn = buildIconButton("×", () => {
28011 pendingPicks = pendingPicks.filter(
28012 (p) => !(p.kind === pick.kind && p.ref === pick.ref)
28013 );
28014 renderBody();
28015 });
28016 row.appendChild(removeBtn);
28017 pendingBlock.appendChild(row);
28018 }
28019 const sendBtn = document.createElement("wpd-button");
28020 sendBtn.setAttribute("variant", "primary");
28021 sendBtn.textContent = `Send ${pendingPicks.length} invite${pendingPicks.length === 1 ? "" : "s"}`;
28022 sendBtn.style.marginTop = "8px";
28023 sendBtn.addEventListener("click", async () => {
28024 if (pendingPicks.length === 0) {
28025 return;
28026 }
28027 sendBtn.setAttribute("busy", "");
28028 sendBtn.setAttribute("disabled", "");
28029 const snapshot = pendingPicks.slice();
28030 let succeeded = 0;
28031 let firstError = null;
28032 for (const pick of snapshot) {
28033 try {
28034 await inviteShare(opts.folderId, {
28035 principalType: pick.kind,
28036 principalRef: pick.ref,
28037 capability: pick.cap
28038 });
28039 succeeded++;
28040 } catch (err) {
28041 firstError = err;
28042 break;
28043 }
28044 }
28045 if (succeeded > 0) {
28046 pendingPicks = pendingPicks.slice(succeeded);
28047 }
28048 try {
28049 await refresh();
28050 } catch (_e) {
28051 }
28052 if (firstError) {
28053 showToast({
28054 message: `Could not send invites: ${firstError.message}`
28055 });
28056 } else {
28057 showToast({
28058 message: 1 === succeeded ? "Invite sent." : `${succeeded} invites sent.`
28059 });
28060 }
28061 sendBtn.removeAttribute("busy");
28062 sendBtn.removeAttribute("disabled");
28063 renderBody();
28064 });
28065 pendingBlock.appendChild(sendBtn);
28066 modal.appendChild(pendingBlock);
28067 }
28068 const listTitle = document.createElement("div");
28069 listTitle.textContent = "Who has access";
28070 listTitle.style.cssText = "font-weight:600;margin:8px 0 6px;";
28071 modal.appendChild(listTitle);
28072 if (shares.length === 0) {
28073 const empty = document.createElement("div");
28074 empty.textContent = "Only you can see this folder.";
28075 empty.style.cssText = "opacity:0.6;font-size:12px;";
28076 modal.appendChild(empty);
28077 } else {
28078 for (const s of shares) {
28079 const row = document.createElement("div");
28080 row.style.cssText = "display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.04);";
28081 const label = document.createElement("div");
28082 label.style.flex = "1";
28083 label.textContent = s.principalType === "role" ? `Role: ${s.displayName}` : s.displayName;
28084 if (s.state === "pending") {
28085 const tag = document.createElement("span");
28086 tag.textContent = " · pending";
28087 tag.style.cssText = "opacity:0.6;font-size:12px;";
28088 label.appendChild(tag);
28089 } else if (s.state === "denied") {
28090 const tag = document.createElement("span");
28091 tag.textContent = " · denied";
28092 tag.style.cssText = "color:#d63638;font-size:12px;";
28093 label.appendChild(tag);
28094 }
28095 row.appendChild(label);
28096 const cap = s.capability === "write" ? "write" : "read";
28097 const capSeg = buildCapSegmented(cap, (next) => {
28098 void changeCap(s, next);
28099 });
28100 row.appendChild(capSeg);
28101 const removeBtn = buildIconButton(
28102 "×",
28103 () => {
28104 void revoke(s);
28105 },
28106 { danger: true }
28107 );
28108 row.appendChild(removeBtn);
28109 modal.appendChild(row);
28110 }
28111 }
28112 const footer = document.createElement("div");
28113 footer.setAttribute("slot", "footer");
28114 footer.style.display = "flex";
28115 footer.style.justifyContent = "flex-end";
28116 footer.style.gap = "10px";
28117 footer.style.flexWrap = "wrap";
28118 const doneBtn = document.createElement("wpd-button");
28119 doneBtn.setAttribute("variant", "secondary");
28120 doneBtn.textContent = "Done";
28121 doneBtn.addEventListener("click", () => modal.remove());
28122 footer.appendChild(doneBtn);
28123 modal.appendChild(footer);
28124 };
28125 const refresh = async () => {
28126 try {
28127 const res = await listShares(opts.folderId);
28128 shares = res.shares;
28129 setSharesForFolder(opts.folderId, shares);
28130 } catch (err) {
28131 showToast({
28132 message: `Could not load shares: ${err.message}`
28133 });
28134 }
28135 renderBody();
28136 };
28137 const revoke = async (s) => {
28138 try {
28139 await revokeShare(opts.folderId, s.id);
28140 removeShare(opts.folderId, s.id);
28141 await refresh();
28142 showToast({ message: "Access revoked." });
28143 } catch (err) {
28144 showToast({
28145 message: `Could not revoke: ${err.message}`
28146 });
28147 }
28148 };
28149 const changeCap = async (s, cap) => {
28150 try {
28151 const next = await updateShareCapability(opts.folderId, s.id, cap);
28152 upsertShare(next);
28153 await refresh();
28154 } catch (err) {
28155 showToast({
28156 message: `Could not update capability: ${err.message}`
28157 });
28158 }
28159 };
28160 modal.addEventListener("wpd-modal-cancel", () => modal.remove());
28161 renderBody();
28162 await refresh();
28163 }
28164 async function openFileShareModal(opts) {
28165 const modal = document.createElement("wpd-modal");
28166 modal.setAttribute("open", "");
28167 modal.setAttribute("size", "md");
28168 modal.setAttribute("title", `Share "${opts.fileName}"`);
28169 document.body.appendChild(modal);
28170 let shares = [];
28171 const refresh = async () => {
28172 try {
28173 const res = await listFileShares(opts.fileId);
28174 shares = res.shares;
28175 } catch (err) {
28176 showToast({
28177 message: `Could not load shares: ${err.message}`
28178 });
28179 }
28180 renderBody();
28181 };
28182 const renderBody = () => {
28183 modal.innerHTML = "";
28184 const note = document.createElement("div");
28185 note.style.cssText = "opacity:0.7;margin-bottom:14px;font-size:12px;";
28186 note.textContent = "People you share with can view and download this file. Only you can move, rename, or delete it.";
28187 modal.appendChild(note);
28188 const addLabel = document.createElement("div");
28189 addLabel.textContent = "Add people";
28190 addLabel.style.cssText = "font-weight:600;margin-bottom:6px;";
28191 modal.appendChild(addLabel);
28192 const userSearch = document.createElement("wpd-user-search");
28193 userSearch.setAttribute(
28194 "exclude",
28195 shares.map((s) => s.principalRef).join(",")
28196 );
28197 userSearch.setAttribute("placeholder", "Search users…");
28198 userSearch.addEventListener("wpd-user-pick", (e) => {
28199 const detail = e.detail;
28200 void (async () => {
28201 try {
28202 await inviteFileShare(opts.fileId, detail.user.id);
28203 showToast({ message: `Invite sent to ${detail.user.name}.` });
28204 } catch (err) {
28205 showToast({
28206 message: `Could not invite: ${err.message}`
28207 });
28208 }
28209 await refresh();
28210 })();
28211 });
28212 modal.appendChild(userSearch);
28213 const listTitle = document.createElement("div");
28214 listTitle.textContent = "Who has access";
28215 listTitle.style.cssText = "font-weight:600;margin:14px 0 6px;";
28216 modal.appendChild(listTitle);
28217 if (shares.length === 0) {
28218 const empty = document.createElement("div");
28219 empty.textContent = "Only you can see this file.";
28220 empty.style.cssText = "opacity:0.6;font-size:12px;";
28221 modal.appendChild(empty);
28222 } else {
28223 for (const s of shares) {
28224 const row = document.createElement("div");
28225 row.style.cssText = "display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.04);";
28226 const label = document.createElement("div");
28227 label.style.flex = "1";
28228 const display = s.displayName;
28229 label.textContent = display || `User #${s.principalRef}`;
28230 if (s.state === "pending") {
28231 const tag = document.createElement("span");
28232 tag.textContent = " · pending";
28233 tag.style.cssText = "opacity:0.6;font-size:12px;";
28234 label.appendChild(tag);
28235 } else if (s.state === "denied") {
28236 const tag = document.createElement("span");
28237 tag.textContent = " · denied";
28238 tag.style.cssText = "color:#d63638;font-size:12px;";
28239 label.appendChild(tag);
28240 }
28241 row.appendChild(label);
28242 const cap = document.createElement("span");
28243 cap.textContent = "Read + download";
28244 cap.style.cssText = "opacity:0.6;font-size:12px;";
28245 row.appendChild(cap);
28246 const removeBtn = buildIconButton(
28247 "×",
28248 () => {
28249 void (async () => {
28250 try {
28251 await revokeFileShare(opts.fileId, s.id);
28252 showToast({ message: "Access revoked." });
28253 } catch (err) {
28254 showToast({
28255 message: `Could not revoke: ${err.message}`
28256 });
28257 }
28258 await refresh();
28259 })();
28260 },
28261 { danger: true }
28262 );
28263 row.appendChild(removeBtn);
28264 modal.appendChild(row);
28265 }
28266 }
28267 const footer = document.createElement("div");
28268 footer.setAttribute("slot", "footer");
28269 footer.style.cssText = "display:flex;justify-content:flex-end;gap:10px;flex-wrap:wrap;";
28270 const doneBtn = document.createElement("wpd-button");
28271 doneBtn.setAttribute("variant", "secondary");
28272 doneBtn.textContent = "Done";
28273 doneBtn.addEventListener("click", () => modal.remove());
28274 footer.appendChild(doneBtn);
28275 modal.appendChild(footer);
28276 };
28277 modal.addEventListener("wpd-modal-cancel", () => modal.remove());
28278 renderBody();
28279 await refresh();
28280 }
28281 function openPendingFileInviteModal(invite) {
28282 return new Promise((resolve2) => {
28283 const modal = document.createElement("wpd-modal");
28284 modal.setAttribute("open", "");
28285 modal.setAttribute(
28286 "title",
28287 invite.fileName ? `${invite.ownerName ?? "Someone"} shared "${invite.fileName}" with you` : "File shared with you"
28288 );
28289 const body = document.createElement("div");
28290 body.innerHTML = `
28291 <p style="margin: 0 0 12px;">Accept the invite to add this file to your desktop.</p>
28292 <p style="margin: 0; opacity: 0.75;">Access level: <strong>Read + download</strong></p>
28293 `;
28294 modal.appendChild(body);
28295 const footer = document.createElement("div");
28296 footer.setAttribute("slot", "footer");
28297 footer.style.cssText = "display:flex;justify-content:flex-end;gap:10px;flex-wrap:wrap;";
28298 const laterBtn = document.createElement("wpd-button");
28299 laterBtn.setAttribute("variant", "secondary");
28300 laterBtn.textContent = "Decide later";
28301 laterBtn.addEventListener("click", () => {
28302 modal.remove();
28303 resolve2("dismissed");
28304 });
28305 const denyBtn = document.createElement("wpd-button");
28306 denyBtn.setAttribute("variant", "danger");
28307 denyBtn.textContent = "Deny";
28308 denyBtn.addEventListener("click", async () => {
28309 denyBtn.setAttribute("busy", "");
28310 denyBtn.setAttribute("disabled", "");
28311 try {
28312 await denyFileShare(invite.fileId, invite.id);
28313 sharesStore().state.deniedFiles.add(invite.fileId);
28314 sharesStore().notify();
28315 modal.remove();
28316 resolve2("denied");
28317 } catch (err) {
28318 showToast({
28319 message: `Could not deny: ${err.message}`
28320 });
28321 denyBtn.removeAttribute("busy");
28322 denyBtn.removeAttribute("disabled");
28323 }
28324 });
28325 const acceptBtn = document.createElement("wpd-button");
28326 acceptBtn.setAttribute("variant", "primary");
28327 acceptBtn.textContent = "Accept";
28328 acceptBtn.addEventListener("click", async () => {
28329 acceptBtn.setAttribute("busy", "");
28330 acceptBtn.setAttribute("disabled", "");
28331 try {
28332 await acceptFileShare(invite.fileId, invite.id);
28333 try {
28334 const res = await listPlacements(0);
28335 setFolderPlacements(0, res.placements);
28336 } catch (_e) {
28337 }
28338 modal.remove();
28339 resolve2("accepted");
28340 } catch (err) {
28341 showToast({
28342 message: `Could not accept: ${err.message}`
28343 });
28344 acceptBtn.removeAttribute("busy");
28345 acceptBtn.removeAttribute("disabled");
28346 }
28347 });
28348 footer.appendChild(laterBtn);
28349 footer.appendChild(denyBtn);
28350 footer.appendChild(acceptBtn);
28351 modal.appendChild(footer);
28352 modal.addEventListener("wpd-modal-cancel", () => {
28353 modal.remove();
28354 resolve2("dismissed");
28355 });
28356 document.body.appendChild(modal);
28357 });
28358 }
28359 function openPendingInviteModal(invite) {
28360 return new Promise((resolve2) => {
28361 const modal = document.createElement("wpd-modal");
28362 modal.setAttribute("open", "");
28363 modal.setAttribute("title", invite.folderName ? `${invite.ownerName ?? "Someone"} shared "${invite.folderName}" with you` : "Folder shared with you");
28364 const body = document.createElement("div");
28365 const capLabel = invite.capability === "write" ? "Read + Write" : "Read";
28366 body.innerHTML = `
28367 <p style="margin: 0 0 12px;">Accept the invite to add this folder to your desktop.</p>
28368 <p style="margin: 0; opacity: 0.75;">Access level: <strong>${capLabel}</strong></p>
28369 `;
28370 modal.appendChild(body);
28371 const footer = document.createElement("div");
28372 footer.setAttribute("slot", "footer");
28373 footer.style.display = "flex";
28374 footer.style.justifyContent = "flex-end";
28375 footer.style.gap = "10px";
28376 footer.style.flexWrap = "wrap";
28377 const laterBtn = document.createElement("wpd-button");
28378 laterBtn.setAttribute("variant", "secondary");
28379 laterBtn.textContent = "Decide later";
28380 laterBtn.addEventListener("click", () => {
28381 modal.remove();
28382 resolve2("dismissed");
28383 });
28384 const denyBtn = document.createElement("wpd-button");
28385 denyBtn.setAttribute("variant", "danger");
28386 denyBtn.textContent = "Deny";
28387 denyBtn.addEventListener("click", async () => {
28388 denyBtn.setAttribute("busy", "");
28389 denyBtn.setAttribute("disabled", "");
28390 try {
28391 await denyShare(invite.folderId, invite.id);
28392 sharesStore().state.deniedFolders.add(invite.folderId);
28393 sharesStore().notify();
28394 modal.remove();
28395 resolve2("denied");
28396 } catch (err) {
28397 showToast({
28398 message: `Could not deny: ${err.message}`
28399 });
28400 denyBtn.removeAttribute("busy");
28401 denyBtn.removeAttribute("disabled");
28402 }
28403 });
28404 const acceptBtn = document.createElement("wpd-button");
28405 acceptBtn.setAttribute("variant", "primary");
28406 acceptBtn.textContent = "Accept";
28407 acceptBtn.addEventListener("click", async () => {
28408 acceptBtn.setAttribute("busy", "");
28409 acceptBtn.setAttribute("disabled", "");
28410 try {
28411 await acceptShare(invite.folderId, invite.id);
28412 try {
28413 const res = await listPlacements(0);
28414 setFolderPlacements(0, res.placements);
28415 } catch (_e) {
28416 }
28417 modal.remove();
28418 resolve2("accepted");
28419 } catch (err) {
28420 showToast({
28421 message: `Could not accept: ${err.message}`
28422 });
28423 acceptBtn.removeAttribute("busy");
28424 acceptBtn.removeAttribute("disabled");
28425 }
28426 });
28427 footer.appendChild(laterBtn);
28428 footer.appendChild(denyBtn);
28429 footer.appendChild(acceptBtn);
28430 modal.appendChild(footer);
28431 modal.addEventListener("wpd-modal-cancel", () => {
28432 modal.remove();
28433 resolve2("dismissed");
28434 });
28435 document.body.appendChild(modal);
28436 });
28437 }
28438 const shareSettingsModal = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
28439 __proto__: null,
28440 openFileShareModal,
28441 openPendingFileInviteModal,
28442 openPendingInviteModal,
28443 openShareSettingsModal
28444 }, Symbol.toStringTag, { value: "Module" }));
28445 function viewerId$1() {
28446 return Number(window.desktopModeConfig?.currentUserId ?? 0);
28447 }
28448 function sharingEnabled$2() {
28449 const settings = window.wp?.desktop?.getOsSettings?.();
28450 if (!settings) {
28451 return true;
28452 }
28453 return settings.foldersSharingEnabled !== false;
28454 }
28455 function folderOwnerId(folderId) {
28456 const folder = getFilesState().folders.get(folderId);
28457 return folder ? Number(folder.ownerId) : 0;
28458 }
28459 function folderIdFromBaseId(baseId) {
28460 if (typeof baseId !== "string") {
28461 return null;
28462 }
28463 const m = /^desktop-mode-folder-(\d+)$/.exec(baseId);
28464 return m ? Number(m[1]) : null;
28465 }
28466 function placementFolderId(placement) {
28467 if (placement.file.type !== "folder") {
28468 return null;
28469 }
28470 const ref = Number(placement.file.ref);
28471 if (!Number.isFinite(ref) || ref <= 0) {
28472 return null;
28473 }
28474 return ref;
28475 }
28476 function placementOwnerId(placement) {
28477 return Number(placement.file.ownerId ?? 0);
28478 }
28479 function installShareMenuItems() {
28480 addFilter(
28481 "desktop-mode.files.tile-menu",
28482 "desktop-mode/folder-share",
28483 (items, placement) => {
28484 if (!sharingEnabled$2()) {
28485 return items;
28486 }
28487 const folderId = placementFolderId(placement);
28488 if (folderId === null) {
28489 return items;
28490 }
28491 const ownerId = folderOwnerId(folderId) || placementOwnerId(placement);
28492 const viewer = viewerId$1();
28493 if (ownerId === viewer) {
28494 const shared = !!placement.file.shareSummary?.shared;
28495 const label = shared ? "Manage sharing…" : "Share folder…";
28496 items.push({
28497 id: "desktop-mode/folder-share",
28498 label,
28499 icon: "dashicons-share",
28500 sort: 30,
28501 onClick: () => {
28502 void openShareSettingsModal({
28503 folderId,
28504 folderName: placement.file.title || `Folder ${folderId}`
28505 });
28506 }
28507 });
28508 } else if (ownerId > 0) {
28509 items.push({
28510 id: "desktop-mode/folder-leave",
28511 label: "Leave shared folder",
28512 icon: "dashicons-exit",
28513 sort: 80,
28514 danger: true,
28515 onClick: async () => {
28516 const ok = await wpdConfirm$1({
28517 title: "Leave this folder?",
28518 message: "The folder will be removed from your desktop. The original and its contents are not deleted; the owner keeps them.",
28519 confirmLabel: "Leave",
28520 danger: true
28521 });
28522 if (!ok) {
28523 return;
28524 }
28525 try {
28526 await leaveShare(folderId);
28527 removePlacement(placement.id);
28528 try {
28529 const res = await listPlacements(0);
28530 setFolderPlacements(0, res.placements);
28531 } catch (_e) {
28532 }
28533 const winId = `desktop-mode-folder-${folderId}`;
28534 const mgr = window.desktopMode?.windowManager;
28535 mgr?.close?.(winId);
28536 showToast({ message: "You left the shared folder." });
28537 } catch (err) {
28538 showToast({
28539 message: `Could not leave: ${err.message}`
28540 });
28541 }
28542 }
28543 });
28544 }
28545 return items;
28546 }
28547 );
28548 registerTitleBarButton({
28549 id: "desktop-mode/folder-share",
28550 label: "Share folder",
28551 icon: "dashicons-share",
28552 placement: "right",
28553 order: 50,
28554 match: (w) => {
28555 if (!sharingEnabled$2()) {
28556 return false;
28557 }
28558 const base = w.config.baseId ?? w.id;
28559 const folderId = folderIdFromBaseId(base);
28560 if (folderId === null) {
28561 return false;
28562 }
28563 return folderOwnerId(folderId) === viewerId$1();
28564 },
28565 onClick: (w) => {
28566 const base = w.config.baseId ?? w.id;
28567 const folderId = folderIdFromBaseId(base);
28568 if (folderId === null) {
28569 return;
28570 }
28571 void openShareSettingsModal({
28572 folderId,
28573 folderName: w.config.title || `Folder ${folderId}`
28574 });
28575 }
28576 });
28577 addAction(
28578 "desktop-mode.files.tile-rendered",
28579 "desktop-mode/folder-share",
28580 (payload) => {
28581 const { tile: tile2, placement } = payload;
28582 if (placement.file.type !== "folder") {
28583 return;
28584 }
28585 const summary = placement.file.shareSummary;
28586 if (!summary?.shared) {
28587 return;
28588 }
28589 if (tile2.querySelector(".desktop-mode-file-tile__share-badge")) {
28590 return;
28591 }
28592 const badge = document.createElement("span");
28593 badge.className = "desktop-mode-file-tile__share-badge dashicons dashicons-share";
28594 badge.setAttribute("aria-label", "Shared folder");
28595 badge.title = "Shared folder";
28596 badge.style.cssText = [
28597 "position:absolute",
28598 "top:6px",
28599 "inset-inline-end:6px",
28600 "background:rgba(0,0,0,0.55)",
28601 "color:#fff",
28602 "border-radius:50%",
28603 "width:18px",
28604 "height:18px",
28605 "font-size:12px",
28606 "line-height:18px",
28607 "text-align:center",
28608 "pointer-events:none"
28609 ].join(";");
28610 tile2.appendChild(badge);
28611 }
28612 );
28613 }
28614 const prompted = /* @__PURE__ */ new Set();
28615 function sharingEnabled$1() {
28616 const settings = window.wp?.desktop?.getOsSettings?.();
28617 if (!settings) {
28618 return true;
28619 }
28620 return settings.foldersSharingEnabled !== false;
28621 }
28622 function installShareInviteBanner() {
28623 const store2 = sharesStore();
28624 const handle = (state2) => {
28625 if (!sharingEnabled$1()) {
28626 return;
28627 }
28628 for (const invite of state2.pending) {
28629 if (prompted.has(invite.id)) {
28630 continue;
28631 }
28632 prompted.add(invite.id);
28633 if (invite.targetType === "file" && typeof invite.fileId === "number") {
28634 const fileId = invite.fileId;
28635 void openPendingFileInviteModal({
28636 id: invite.id,
28637 fileId,
28638 fileName: invite.fileName,
28639 ownerName: invite.ownerName
28640 }).then((decision) => {
28641 if (decision === "accepted") {
28642 dropPending(invite.id);
28643 } else if (decision === "denied") {
28644 dropPending(invite.id, { denied: true, fileId });
28645 }
28646 });
28647 continue;
28648 }
28649 void openPendingInviteModal({
28650 id: invite.id,
28651 folderId: invite.folderId,
28652 folderName: invite.folderName,
28653 ownerName: invite.ownerName,
28654 capability: invite.capability
28655 }).then((decision) => {
28656 if (decision === "accepted") {
28657 dropPending(invite.id);
28658 } else if (decision === "denied") {
28659 dropPending(invite.id, { denied: true, folderId: invite.folderId });
28660 }
28661 });
28662 }
28663 };
28664 store2.subscribe(handle);
28665 handle(store2.state);
28666 }
28667 function viewerId() {
28668 return Number(window.desktopModeConfig?.currentUserId ?? 0);
28669 }
28670 function storageConfig() {
28671 return window.desktopModeConfig?.desktopStorage ?? {};
28672 }
28673 function sharingEnabled() {
28674 const settings = window.wp?.desktop?.getOsSettings?.();
28675 if (!settings) {
28676 return true;
28677 }
28678 return settings.foldersSharingEnabled !== false;
28679 }
28680 function uploadFileId(placement) {
28681 if (placement.file.type !== "upload") {
28682 return null;
28683 }
28684 const id = Number(placement.file.ref);
28685 return Number.isFinite(id) && id > 0 ? id : null;
28686 }
28687 function installUploadMenuItems() {
28688 addFilter(
28689 "desktop-mode.files.tile-menu",
28690 "desktop-mode/uploads",
28691 (items, placement) => {
28692 if (placement.file.type === "folder" && storageConfig().zipAvailable) {
28693 const folderId = Number(placement.file.ref);
28694 if (Number.isFinite(folderId) && folderId > 0) {
28695 items.push({
28696 id: "desktop-mode/folder-zip-download",
28697 label: "Download as .zip",
28698 icon: "dashicons-download",
28699 sort: 45,
28700 onClick: () => {
28701 navigateToDownload(getFolderZipUrl(folderId));
28702 }
28703 });
28704 }
28705 return items;
28706 }
28707 const fileId = uploadFileId(placement);
28708 if (fileId === null) {
28709 return items;
28710 }
28711 items.push({
28712 id: "desktop-mode/upload-download",
28713 label: "Download",
28714 icon: "dashicons-download",
28715 sort: 40,
28716 onClick: () => {
28717 navigateToDownload(getUploadDownloadUrl(fileId));
28718 }
28719 });
28720 const ownerId = Number(
28721 placement.file.ownerId ?? 0
28722 );
28723 const viewer = viewerId();
28724 if (ownerId === viewer && sharingEnabled()) {
28725 items.push({
28726 id: "desktop-mode/upload-share",
28727 label: "Share file…",
28728 icon: "dashicons-share",
28729 sort: 30,
28730 onClick: () => {
28731 void Promise.resolve().then(() => shareSettingsModal).then((mod) => {
28732 void mod.openFileShareModal({
28733 fileId,
28734 fileName: placement.file.title || `File ${fileId}`
28735 });
28736 });
28737 }
28738 });
28739 } else if (ownerId > 0 && ownerId !== viewer && placement.parentId === 0) {
28740 items.push({
28741 id: "desktop-mode/upload-leave",
28742 label: "Leave shared file",
28743 icon: "dashicons-exit",
28744 sort: 80,
28745 danger: true,
28746 onClick: async () => {
28747 const ok = await wpdConfirm$1({
28748 title: "Leave this shared file?",
28749 message: "The file will be removed from your desktop. The owner keeps the original.",
28750 confirmLabel: "Leave",
28751 danger: true
28752 });
28753 if (!ok) {
28754 return;
28755 }
28756 try {
28757 await leaveFileShare(fileId);
28758 removePlacement(placement.id);
28759 try {
28760 const res = await listPlacements(0);
28761 setFolderPlacements(0, res.placements);
28762 } catch (_e) {
28763 }
28764 showToast({ message: "You left the shared file." });
28765 } catch (err) {
28766 showToast({
28767 message: `Could not leave: ${err.message}`
28768 });
28769 }
28770 }
28771 });
28772 }
28773 return items;
28774 }
28775 );
28776 addFilter(
28777 "desktop-mode.wallpaper-context-menu",
28778 "desktop-mode/uploads",
28779 (items) => {
28780 if (!storageConfig().canUpload) {
28781 return items;
28782 }
28783 items.push({
28784 id: "desktop-mode/upload-files",
28785 label: "Upload files…",
28786 icon: "dashicons-upload",
28787 sort: 15,
28788 onClick: () => openFilePicker(false)
28789 });
28790 items.push({
28791 id: "desktop-mode/upload-folder",
28792 label: "Upload folder…",
28793 icon: "dashicons-portfolio",
28794 sort: 16,
28795 onClick: () => openFilePicker(true)
28796 });
28797 return items;
28798 }
28799 );
28800 }
28801 function openFilePicker(directory) {
28802 const input = document.createElement("input");
28803 input.type = "file";
28804 if (directory) {
28805 input.setAttribute("webkitdirectory", "");
28806 } else {
28807 input.multiple = true;
28808 }
28809 input.style.display = "none";
28810 document.body.appendChild(input);
28811 input.addEventListener("change", () => {
28812 const files = input.files ? Array.from(input.files) : [];
28813 input.remove();
28814 if (files.length === 0) {
28815 return;
28816 }
28817 void routePickedFiles(files, directory);
28818 });
28819 input.click();
28820 }
28821 async function routePickedFiles(files, directory) {
28822 const config = window.desktopModeConfig;
28823 const dropConfig = config?.dropConfig ?? {
28824 enabled: false,
28825 allowedMimes: [],
28826 maxSize: 0
28827 };
28828 const manager$1 = await Promise.resolve().then(() => manager);
28829 const { accepted, rejected } = manager$1.partitionByPolicy(files, dropConfig);
28830 if (rejected.length > 0) {
28831 showToast({
28832 message: rejected.length === 1 ? rejected[0].message : `${rejected.length} files can't be uploaded.`
28833 });
28834 }
28835 if (accepted.length === 0) {
28836 return;
28837 }
28838 const entries = accepted.map(({ file, mime }) => ({
28839 file,
28840 mime,
28841 fields: manager$1.defaultFields(file, mime),
28842 // `webkitRelativePath` is populated by directory picks (and
28843 // ONLY by them — drag-drops leave it empty).
28844 relativePath: directory ? file.webkitRelativePath ?? "" : ""
28845 }));
28846 const dialog$1 = await Promise.resolve().then(() => dialog);
28847 await dialog$1.openUploadDialog({
28848 entries,
28849 // Root-targeted picker: desktop destination default, server
28850 // picks free grid slots (no coords on non-wallpaper surfaces).
28851 context: { surface: "folder", folderId: 0, x: 0, y: 0 },
28852 mediaUrl: config?.mediaUrl ?? "",
28853 restNonce: config?.restNonce ?? "",
28854 filesUrl: config?.filesUrl,
28855 storage: config?.desktopStorage,
28856 forceDesktop: directory,
28857 // These pickers live in the desktop's own menu — their whole
28858 // point is desktop storage, media-kind files included.
28859 preferDesktop: true,
28860 mediaMaxBytes: dropConfig.maxSize
28861 });
28862 }
28863 registerBuiltInFileTypes();
28864 registerBuiltInFileOpeners();
28865 installEmbedPersistence();
28866 registerFileAssociationsTab();
28867 installShareMenuItems();
28868 installUploadMenuItems();
28869 const seededPending = window.desktopModeConfig?.serverPendingShares;
28870 if (Array.isArray(seededPending) && seededPending.length > 0) {
28871 ingestPendingInvites(seededPending);
28872 }
28873 installShareInviteBanner();
28874 const filesApi = {
28875 DesktopFile,
28876 registerType,
28877 unregisterType,
28878 getType,
28879 getTypes,
28880 resolve,
28881 subscribe,
28882 registerOpener,
28883 unregisterOpener,
28884 getOpener,
28885 getOpeners,
28886 getOpenersForType,
28887 resolveOpener,
28888 subscribeOpeners,
28889 getUserAssociations,
28890 open: openFile,
28891 rest: filesRest,
28892 store: {
28893 get: getFilesStore,
28894 getState: getFilesState,
28895 subscribe: subscribeFilesStore,
28896 setFolderPlacements,
28897 upsertPlacement,
28898 removePlacement,
28899 setFolders,
28900 upsertFolder,
28901 removeFolder
28902 }
28903 };
28904 const SYNTH_META_KEY = "__synthFromDockItem";
28905 function hashToNegativeId(s) {
28906 let h = 0;
28907 for (let i = 0; i < s.length; i++) {
28908 h = (h * 31 + s.charCodeAt(i)) % 2147483647;
28909 }
28910 return -(h + 1);
28911 }
28912 function buildSyntheticPlacement(item, persistedPositions) {
28913 const saved = persistedPositions[item.id];
28914 return {
28915 id: hashToNegativeId(item.id),
28916 parentId: 0,
28917 x: saved ? saved.x : 0,
28918 y: saved ? saved.y : 0,
28919 sortOrder: 9999,
28920 updatedAtMs: Date.now(),
28921 meta: { [SYNTH_META_KEY]: item.id },
28922 file: {
28923 type: "shortcut",
28924 ref: `dock-promoted:${item.id}`,
28925 title: item.title,
28926 icon: item.icon,
28927 previewUrl: "",
28928 exists: true,
28929 // The shortcut opener (built-in-openers.ts) reads these
28930 // off the file shape — `shortcutUrl` is what a dock-item
28931 // promotion naturally has.
28932 shortcutUrl: item.url
28933 }
28934 };
28935 }
28936 function readDockItems() {
28937 const api = window.wp?.desktop;
28938 if (api?.getMenuItems) {
28939 const items = api.getMenuItems();
28940 return items.map((i) => ({
28941 id: i.id,
28942 title: i.title,
28943 icon: i.icon,
28944 url: i.url,
28945 badge: i.badge ?? 0,
28946 submenu: i.submenu ?? [],
28947 isCore: i.isCore
28948 }));
28949 }
28950 const cfg = window.desktopModeConfig;
28951 return cfg?.dockItems ?? [];
28952 }
28953 function readServerIcons() {
28954 const cfg = window.desktopModeConfig;
28955 return cfg?.desktopIcons ?? [];
28956 }
28957 let reentrant = false;
28958 const removedServerPlacementsByRef = /* @__PURE__ */ new Map();
28959 function prunePromotedPositions(ids) {
28960 const api = window.wp?.desktop;
28961 if (!api?.getOsSettings || !api?.updateOsSettings) {
28962 return;
28963 }
28964 const current = api.getOsSettings().dockPromotedPositions ?? {};
28965 const next = { ...current };
28966 let changed = false;
28967 for (const id of ids) {
28968 if (id in next) {
28969 delete next[id];
28970 changed = true;
28971 }
28972 }
28973 if (changed) {
28974 api.updateOsSettings({ dockPromotedPositions: next });
28975 }
28976 }
28977 function syncShortcutsWithVisibility(visibility, positions = {}, layout) {
28978 if (reentrant) {
28979 return;
28980 }
28981 reentrant = true;
28982 try {
28983 const dockItems = readDockItems();
28984 const serverIcons = readServerIcons();
28985 const dockItemsById = new Map(
28986 dockItems.map((item) => [item.id, item])
28987 );
28988 const state2 = filesApi.store.getState();
28989 const root = state2.placementsByFolder.get(0) ?? [];
28990 const currentSynth = /* @__PURE__ */ new Map();
28991 for (const p of root) {
28992 const sourceId = (p.meta ?? null) && typeof p.meta === "object" ? p.meta[SYNTH_META_KEY] : null;
28993 if (typeof sourceId === "string") {
28994 currentSynth.set(sourceId, p);
28995 }
28996 }
28997 const realByRef = /* @__PURE__ */ new Map();
28998 const registeredIconIds = new Set(
28999 serverIcons.map((i) => i.id)
29000 );
29001 for (const p of root) {
29002 const ref = p?.file?.ref;
29003 if (typeof ref === "string" && registeredIconIds.has(ref)) {
29004 realByRef.set(ref, p);
29005 }
29006 }
29007 const desiredSynth = /* @__PURE__ */ new Set();
29008 for (const item of dockItems) {
29009 const resolved = resolvePlacement(item.id, "dock", visibility);
29010 const explicitlyPromoted = resolved === "desktop" || resolved === "both";
29011 const spatialCore = layout === "spatial" && Boolean(item.isCore) && (resolved === "dock" || resolved === "both");
29012 if (explicitlyPromoted || spatialCore) {
29013 desiredSynth.add(item.id);
29014 if (!currentSynth.has(item.id)) {
29015 filesApi.store.upsertPlacement(
29016 buildSyntheticPlacement(item, positions)
29017 );
29018 }
29019 }
29020 }
29021 const positionsToPrune = [];
29022 for (const [sourceId, p] of currentSynth) {
29023 if (desiredSynth.has(sourceId)) {
29024 continue;
29025 }
29026 filesApi.store.removePlacement(p.id);
29027 const sourceItem = dockItemsById.get(sourceId);
29028 const wasOnlySpatialCore = Boolean(sourceItem?.isCore) && visibility[sourceId] === void 0;
29029 if (positions[sourceId] && !wasOnlySpatialCore) {
29030 positionsToPrune.push(sourceId);
29031 }
29032 }
29033 if (positionsToPrune.length > 0) {
29034 prunePromotedPositions(positionsToPrune);
29035 }
29036 for (const icon of serverIcons) {
29037 const placement = visibility[icon.id];
29038 const inStore = realByRef.get(icon.id);
29039 if (placement === "dock" || placement === "hidden") {
29040 if (inStore) {
29041 removedServerPlacementsByRef.set(icon.id, inStore);
29042 filesApi.store.removePlacement(inStore.id);
29043 }
29044 continue;
29045 }
29046 if (!inStore) {
29047 const cached = removedServerPlacementsByRef.get(icon.id);
29048 if (cached) {
29049 filesApi.store.upsertPlacement(cached);
29050 removedServerPlacementsByRef.delete(icon.id);
29051 }
29052 }
29053 }
29054 } finally {
29055 reentrant = false;
29056 }
29057 }
29058 function installShortcutsSync(getVisibility, getPositions = () => ({}), getLayout = () => void 0) {
29059 queueMicrotask(
29060 () => syncShortcutsWithVisibility(
29061 getVisibility(),
29062 getPositions(),
29063 getLayout()
29064 )
29065 );
29066 const off = filesApi.store.subscribe(() => {
29067 syncShortcutsWithVisibility(
29068 getVisibility(),
29069 getPositions(),
29070 getLayout()
29071 );
29072 });
29073 return off;
29074 }
29075 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%}`;
29076 const DEFAULT_EVENT = "desktop-mode-os-settings-save-lifecycle";
29077 const DEFAULT_AUTO_CLEAR_SAVED_MS = 2200;
29078 const DEFAULT_AUTO_CLEAR_FAILED_MS = 6e3;
29079 const _WpdSaveStatus = class _WpdSaveStatus extends Component {
29080 constructor() {
29081 super(...arguments);
29082 this._autoTimer = null;
29083 this._docListener = null;
29084 }
29085 connectedCallback() {
29086 super.connectedCallback();
29087 if (this.auto !== null) {
29088 this._installAutoListener();
29089 }
29090 }
29091 disconnectedCallback() {
29092 this._removeAutoListener();
29093 if (this._autoTimer !== null) {
29094 window.clearTimeout(this._autoTimer);
29095 this._autoTimer = null;
29096 }
29097 }
29098 attributeChangedCallback(name, oldValue, newValue) {
29099 super.attributeChangedCallback(name, oldValue, newValue);
29100 if (name === "auto" || name === "event") {
29101 this._removeAutoListener();
29102 if (this.auto !== null) {
29103 this._installAutoListener();
29104 }
29105 }
29106 if (name === "phase") {
29107 this._scheduleAutoClear();
29108 const detail = {
29109 phase: this.phase ?? "idle",
29110 error: this.error ?? void 0
29111 };
29112 this.emit("wpd-save-status-change", detail);
29113 }
29114 }
29115 render() {
29116 const phase = this.phase ?? "idle";
29117 const mode = this.mode ?? "dot";
29118 const error = this.error ?? "";
29119 const title = error || this._labelForPhase(phase);
29120 if (title) {
29121 this.setAttribute("title", title);
29122 } else {
29123 this.removeAttribute("title");
29124 }
29125 this.setAttribute("aria-live", phase === "failed" ? "assertive" : "polite");
29126 this.setAttribute("role", phase === "failed" ? "alert" : "status");
29127 return html`
29128 <span class="wpd-save-status">
29129 <span class="wpd-save-status__indicator" aria-hidden="true">
29130 <span class="wpd-save-status__glyph">${this._renderGlyph(phase)}</span>
29131 </span>
29132 ${mode === "pill" ? html`<span class="wpd-save-status__label"
29133 >${this._labelForPhase(phase)}</span
29134 >` : html``}
29135 </span>
29136 `;
29137 }
29138 _renderGlyph(phase) {
29139 if (phase === "saved") {
29140 return _iconCheck();
29141 }
29142 if (phase === "failed") {
29143 return _iconBang();
29144 }
29145 return "";
29146 }
29147 _labelForPhase(phase) {
29148 switch (phase) {
29149 case "pending":
29150 case "saving":
29151 return this["saving-label"] ?? "Saving…";
29152 case "saved":
29153 return this["saved-label"] ?? "Saved";
29154 case "failed": {
29155 const err = this.error ?? "";
29156 return err || "Couldn’t save";
29157 }
29158 default:
29159 return this["idle-label"] ?? "";
29160 }
29161 }
29162 _installAutoListener() {
29163 const eventName = this.event || DEFAULT_EVENT;
29164 this._docListener = (e) => {
29165 const detail = e.detail;
29166 if (!detail || typeof detail.phase !== "string") {
29167 return;
29168 }
29169 this.phase = detail.phase;
29170 if (detail.error) {
29171 this.error = detail.error;
29172 } else if (detail.phase !== "failed" && this.error) {
29173 this.removeAttribute("error");
29174 }
29175 };
29176 document.addEventListener(eventName, this._docListener);
29177 }
29178 _removeAutoListener() {
29179 if (!this._docListener) {
29180 return;
29181 }
29182 const eventName = this.event || DEFAULT_EVENT;
29183 document.removeEventListener(eventName, this._docListener);
29184 this._docListener = null;
29185 }
29186 _scheduleAutoClear() {
29187 if (this._autoTimer !== null) {
29188 window.clearTimeout(this._autoTimer);
29189 this._autoTimer = null;
29190 }
29191 const phase = this.phase ?? "idle";
29192 const ms = this._autoClearMsFor(phase);
29193 if (ms <= 0) {
29194 return;
29195 }
29196 this._autoTimer = window.setTimeout(() => {
29197 this._autoTimer = null;
29198 this.phase = "idle";
29199 }, ms);
29200 }
29201 _autoClearMsFor(phase) {
29202 if (phase === "saved") {
29203 const raw = this["auto-clear-saved-ms"];
29204 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_SAVED_MS;
29205 }
29206 if (phase === "failed") {
29207 const raw = this["auto-clear-failed-ms"];
29208 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_FAILED_MS;
29209 }
29210 return 0;
29211 }
29212 };
29213 _WpdSaveStatus.props = [
29214 "phase",
29215 "mode",
29216 "animation",
29217 "auto",
29218 "event",
29219 "error",
29220 "saving-label",
29221 "saved-label",
29222 "idle-label",
29223 "auto-clear-saved-ms",
29224 "auto-clear-failed-ms"
29225 ];
29226 _WpdSaveStatus.styles = [styles$2];
29227 _WpdSaveStatus.help = {
29228 title: "Save status",
29229 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.',
29230 status: "experimental",
29231 since: "0.8.0",
29232 props: [
29233 {
29234 name: "phase",
29235 type: "'idle' | 'pending' | 'saving' | 'saved' | 'failed'",
29236 default: "idle",
29237 description: "Current lifecycle phase. Set manually for one-off integrations, or rely on `auto` to populate it from a CustomEvent."
29238 },
29239 {
29240 name: "mode",
29241 type: "'dot' | 'icon' | 'pill'",
29242 default: "dot",
29243 description: "Layout. `dot` is the smallest (10×10 colored dot); `icon` adds a glyph inside on saved/failed; `pill` adds an inline label."
29244 },
29245 {
29246 name: "animation",
29247 type: "'pulse' | 'modem'",
29248 default: "pulse",
29249 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."
29250 },
29251 {
29252 name: "auto",
29253 type: "boolean attribute",
29254 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="…"`.'
29255 },
29256 {
29257 name: "event",
29258 type: "string",
29259 default: "desktop-mode-os-settings-save-lifecycle",
29260 description: "CustomEvent name to listen on when `auto` is set."
29261 },
29262 {
29263 name: "error",
29264 type: "string",
29265 description: "Error message shown in `pill` mode and exposed as the host title attribute (so dot/icon modes still surface the message via tooltip)."
29266 },
29267 {
29268 name: "saving-label",
29269 type: "string",
29270 default: "Saving…",
29271 description: "Pill-mode label shown during `pending` / `saving`."
29272 },
29273 {
29274 name: "saved-label",
29275 type: "string",
29276 default: "Saved",
29277 description: "Pill-mode label shown during `saved`."
29278 },
29279 {
29280 name: "idle-label",
29281 type: "string",
29282 description: 'Optional pill-mode label shown during `idle` (e.g. "All changes saved"). When unset, the pill collapses to invisible while idle.'
29283 },
29284 {
29285 name: "auto-clear-saved-ms",
29286 type: "integer",
29287 default: "2200",
29288 description: "How long the `saved` phase stays visible before auto-fading back to `idle`."
29289 },
29290 {
29291 name: "auto-clear-failed-ms",
29292 type: "integer",
29293 default: "6000",
29294 description: "How long the `failed` phase stays visible before auto-fading back to `idle`."
29295 }
29296 ],
29297 events: [
29298 {
29299 name: "wpd-save-status-change",
29300 description: "Fires when the phase changes (manually or via auto-listen).",
29301 detail: "{ phase, error }"
29302 }
29303 ],
29304 cssProps: [
29305 {
29306 name: "--wpd-save-status-bg",
29307 description: "Indicator background color (saving/pending phase)."
29308 },
29309 {
29310 name: "--wpd-save-status-saved-bg",
29311 description: "Indicator background on saved."
29312 },
29313 {
29314 name: "--wpd-save-status-failed-bg",
29315 description: "Indicator background on failed."
29316 },
29317 {
29318 name: "--wpd-save-status-pill-bg",
29319 description: "Pill background (mode=pill)."
29320 },
29321 {
29322 name: "--wpd-save-status-pill-fg",
29323 description: "Pill foreground (mode=pill)."
29324 }
29325 ],
29326 example: html`
29327 <wpd-cluster gap="12">
29328 <wpd-save-status phase="pending"></wpd-save-status>
29329 <wpd-save-status phase="saving"></wpd-save-status>
29330 <wpd-save-status phase="saved"></wpd-save-status>
29331 <wpd-save-status phase="failed"></wpd-save-status>
29332 <wpd-save-status mode="pill" phase="saving"></wpd-save-status>
29333 <wpd-save-status mode="pill" phase="saved"></wpd-save-status>
29334 <wpd-save-status mode="pill" phase="failed" error="Network error."></wpd-save-status>
29335 </wpd-cluster>
29336 `
29337 };
29338 let WpdSaveStatus = _WpdSaveStatus;
29339 defineComponent("wpd-save-status", WpdSaveStatus);
29340 function _iconCheck() {
29341 return html`
29342 <svg
29343 viewBox="0 0 12 12"
29344 aria-hidden="true"
29345 focusable="false"
29346 fill="none"
29347 stroke="currentColor"
29348 stroke-width="2"
29349 stroke-linecap="round"
29350 stroke-linejoin="round"
29351 >
29352 <path d="M2.5 6 L5 8.5 L9.5 4" />
29353 </svg>
29354 `;
29355 }
29356 function _iconBang() {
29357 return html`
29358 <svg
29359 viewBox="0 0 12 12"
29360 aria-hidden="true"
29361 focusable="false"
29362 fill="currentColor"
29363 >
29364 <path
29365 d="M5 2 H7 V7 H5 z M5 8.5 H7 V10.5 H5 z"
29366 />
29367 </svg>
29368 `;
29369 }
29370 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}`;
29371 const _WpdTextarea = class _WpdTextarea extends Component {
29372 constructor() {
29373 super(...arguments);
29374 this._textareaEl = null;
29375 }
29376 connectedCallback() {
29377 super.connectedCallback();
29378 ensureAutoId(this);
29379 }
29380 render() {
29381 const label = this._attr("label") || "";
29382 const value = this._attr("value") ?? "";
29383 const placeholder = this._attr("placeholder") || "";
29384 const disabled = this._boolAttr("disabled");
29385 const readonly = this._boolAttr("readonly");
29386 const ariaLabel = this._attr("aria-label") || label;
29387 const name = this._attr("name") || "";
29388 const rows = Number(this._attr("rows")) || 3;
29389 const maxLength = this._attr("maxlength");
29390 const minLength = this._attr("minlength");
29391 const invalid = this._boolAttr("invalid");
29392 const hostId = this.id || "wpd-unnamed";
29393 const fieldId = `${hostId}__field`;
29394 return html`
29395 ${label ? html`<label class="wpd-textarea__label" for=${fieldId}>${label}</label>` : html``}
29396 <textarea
29397 id=${fieldId}
29398 part="textarea"
29399 .value=${value}
29400 placeholder=${placeholder}
29401 ?disabled=${disabled}
29402 ?readonly=${readonly}
29403 rows=${rows}
29404 maxlength=${maxLength ?? ""}
29405 minlength=${minLength ?? ""}
29406 name=${name}
29407 aria-invalid=${invalid ? "true" : "false"}
29408 aria-label=${ariaLabel || ""}
29409 @input=${(e) => this._onInput(e)}
29410 @change=${(e) => this._onChange(e)}
29411 @keydown=${(e) => this._onKeyDown(e)}
29412 ></textarea>
29413 `;
29414 }
29415 _attr(name) {
29416 return this.getAttribute(name);
29417 }
29418 _boolAttr(name) {
29419 return this.getAttribute(name) !== null;
29420 }
29421 _onInput(e) {
29422 const ta = e.target;
29423 this._textareaEl = ta;
29424 this.setAttribute("value", ta.value);
29425 this.emit("wpd-input-change", { value: ta.value });
29426 if (this._boolAttr("auto-grow")) {
29427 this._autosize(ta);
29428 }
29429 }
29430 _onChange(e) {
29431 const ta = e.target;
29432 this.emit("wpd-input-commit", { value: ta.value });
29433 }
29434 _onKeyDown(e) {
29435 if (!this._boolAttr("submit-on-enter")) {
29436 return;
29437 }
29438 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) {
29439 e.preventDefault();
29440 const ta = e.target;
29441 this.emit("wpd-submit", { value: ta.value });
29442 }
29443 }
29444 /**
29445 * Grow the textarea height to fit content, capped at `max-rows`.
29446 * Resets to scroll-height each input then clamps; cheap because
29447 * the browser caches layout.
29448 */
29449 _autosize(ta) {
29450 const maxRows = Number(this._attr("max-rows")) || 8;
29451 const cs = window.getComputedStyle(ta);
29452 const fontSize = parseFloat(cs.fontSize) || 13;
29453 const lineHeightRaw = cs.lineHeight;
29454 const lineHeight = lineHeightRaw === "normal" ? fontSize * 1.45 : parseFloat(lineHeightRaw) || fontSize * 1.45;
29455 const paddingTop = parseFloat(cs.paddingTop) || 0;
29456 const paddingBottom = parseFloat(cs.paddingBottom) || 0;
29457 const max = lineHeight * maxRows + paddingTop + paddingBottom;
29458 ta.style.height = "auto";
29459 const next = Math.min(ta.scrollHeight, max);
29460 ta.style.height = `${next}px`;
29461 }
29462 /** Public helper for callers that programmatically set `.value` and want autosize to re-run. */
29463 refreshAutosize() {
29464 if (this._textareaEl && this._boolAttr("auto-grow")) {
29465 this._autosize(this._textareaEl);
29466 }
29467 }
29468 /** Imperatively focus the underlying textarea. */
29469 focusInput() {
29470 const root = this.shadowRoot ?? this;
29471 const ta = root.querySelector("textarea");
29472 ta?.focus();
29473 }
29474 /** Imperatively clear the value. */
29475 clear() {
29476 this.setAttribute("value", "");
29477 const root = this.shadowRoot ?? this;
29478 const ta = root.querySelector("textarea");
29479 if (ta) {
29480 ta.value = "";
29481 if (this._boolAttr("auto-grow")) {
29482 this._autosize(ta);
29483 }
29484 }
29485 }
29486 };
29487 _WpdTextarea.props = [
29488 "label",
29489 "value",
29490 "placeholder",
29491 "disabled",
29492 "readonly",
29493 "ariaLabel",
29494 "name",
29495 "rows",
29496 "maxlength",
29497 "minlength",
29498 "invalid",
29499 "autoGrow",
29500 "maxRows",
29501 "submitOnEnter"
29502 ];
29503 _WpdTextarea.styles = [textareaStyles];
29504 _WpdTextarea.help = {
29505 title: "Textarea",
29506 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).",
29507 status: "stable",
29508 since: "0.6.0",
29509 props: [
29510 { name: "label", type: "string", description: "Visible label above the textarea." },
29511 { name: "value", type: "string", description: "Current value; reflected two-way." },
29512 { name: "placeholder", type: "string", description: "Native placeholder." },
29513 { name: "disabled", type: "boolean attribute" },
29514 { name: "readonly", type: "boolean attribute" },
29515 { name: "aria-label", type: "string", description: "Accessible label when no visible label is rendered." },
29516 { name: "name", type: "string", description: "Forwarded to native textarea for form submission." },
29517 { name: "rows", type: "integer (string)", default: "3", description: "Initial visible row count." },
29518 { name: "maxlength", type: "integer (string)" },
29519 { name: "minlength", type: "integer (string)" },
29520 { name: "invalid", type: "boolean attribute", description: "Sets aria-invalid + error styling." },
29521 { name: "auto-grow", type: "boolean attribute", description: "Grows up to max-rows as the user types." },
29522 { name: "max-rows", type: "integer (string)", default: "8" },
29523 {
29524 name: "submit-on-enter",
29525 type: "boolean attribute",
29526 description: "Enter fires wpd-submit; Shift+Enter inserts a newline."
29527 }
29528 ],
29529 events: [
29530 { name: "wpd-input-change", description: "Fires on every keystroke.", detail: "{ value: string }" },
29531 { name: "wpd-input-commit", description: "Fires on blur / native change.", detail: "{ value: string }" },
29532 {
29533 name: "wpd-submit",
29534 description: "Fires on Enter (without Shift) when submit-on-enter is set.",
29535 detail: "{ value: string }"
29536 }
29537 ],
29538 example: html`
29539 <wpd-textarea label="Message" rows="3" auto-grow max-rows="8" submit-on-enter></wpd-textarea>
29540 `
29541 };
29542 let WpdTextarea = _WpdTextarea;
29543 defineComponent("wpd-textarea", WpdTextarea);
29544 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}`;
29545 const ICONS = {
29546 minimize: '<path d="M3 6h6" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>',
29547 maximize: '<rect x="3" y="3" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.25" fill="none"/>',
29548 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"/>',
29549 "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"/>',
29550 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"/>',
29551 reload: (
29552 // Filled icon scaled from a 512×512 source into the 12×12 viewBox
29553 // shared with the other title-bar glyphs. The wrapping `<g>` does
29554 // the math; the inner path is dropped in unmodified so its
29555 // authoring tool can be re-edited and copy-pasted again.
29556 // `scale(0.021)` ≈ 90% of full fit, with `translate(0.6)` to
29557 // keep the result centered inside the 12×12 viewBox so the
29558 // glyph reads slightly smaller than min/max/close — closer to
29559 // the visual weight of the other title-bar buttons.
29560 '<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>'
29561 ),
29562 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"/>',
29563 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"/>'
29564 };
29565 const _WpdWindowButton = class _WpdWindowButton extends Component {
29566 constructor() {
29567 super(...arguments);
29568 this._activateWired = false;
29569 }
29570 render() {
29571 const iconKey = this.icon || "";
29572 const svgInner = ICONS[iconKey] || "";
29573 return html`
29574 <button type="button">
29575 <svg
29576 width="14"
29577 height="14"
29578 viewBox="0 0 12 12"
29579 aria-hidden="true"
29580 focusable="false"
29581 ></svg>
29582 <slot></slot>
29583 </button>
29584 <span data-svg-buffer style="display:none">${svgInner}</span>
29585 `;
29586 }
29587 /**
29588 * After each render, copy the raw SVG markup into the actual
29589 * `<svg>` element. The templater only writes text into slots,
29590 * so we stash the intended markup in a hidden buffer and
29591 * `innerHTML = ` the svg once here — a one-shot post-render
29592 * hook that keeps the declarative template honest.
29593 *
29594 * Also wires up the `wpd-button-activate` CustomEvent that
29595 * fires exactly once per gesture — the canonical contract
29596 * for plugin-registered title-bar buttons. Plugin authors who
29597 * use `addEventListener( 'click', cb )` directly still get
29598 * what they expect (the title bar's drag-handler now excludes
29599 * chrome buttons by class so static clicks land normally),
29600 * but `wpd-button-activate` is the documented surface that
29601 * documents the once-per-gesture contract explicitly. See
29602 * the class-level docblock for rationale.
29603 */
29604 connectedCallback() {
29605 super.connectedCallback();
29606 queueMicrotask(() => this._paintSvg());
29607 queueMicrotask(() => this._wireActivateEvent());
29608 }
29609 attributeChangedCallback(name, oldValue, newValue) {
29610 super.attributeChangedCallback(name, oldValue, newValue);
29611 queueMicrotask(() => this._paintSvg());
29612 }
29613 _paintSvg() {
29614 const root = this.shadowRoot;
29615 if (!root) {
29616 return;
29617 }
29618 const svg = root.querySelector("svg");
29619 const buffer = root.querySelector("[data-svg-buffer]");
29620 if (svg && buffer) {
29621 const markup = buffer.textContent || "";
29622 if (svg.innerHTML !== markup) {
29623 svg.innerHTML = markup;
29624 }
29625 }
29626 }
29627 _wireActivateEvent() {
29628 if (this._activateWired) {
29629 return;
29630 }
29631 const root = this.shadowRoot;
29632 if (!root) {
29633 return;
29634 }
29635 const button = root.querySelector("button");
29636 if (!button) {
29637 return;
29638 }
29639 this._activateWired = true;
29640 button.addEventListener("click", () => {
29641 this.dispatchEvent(
29642 new CustomEvent("wpd-button-activate", {
29643 bubbles: true,
29644 composed: true,
29645 cancelable: true
29646 })
29647 );
29648 });
29649 }
29650 };
29651 _WpdWindowButton.props = ["icon", "active", "danger"];
29652 _WpdWindowButton.styles = [styles$1];
29653 _WpdWindowButton.help = {
29654 title: "Window button",
29655 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.",
29656 status: "stable",
29657 since: "0.9.0",
29658 props: [
29659 {
29660 name: "icon",
29661 type: "'minimize' | 'maximize' | 'fullscreen' | 'fullscreen-exit' | 'detach' | 'reload' | 'close' | 'menu'",
29662 description: "Which built-in inline SVG to paint. Omit to supply your own via the slot."
29663 },
29664 {
29665 name: "active",
29666 type: "boolean attribute",
29667 description: "Applies the pressed-down look (used e.g. while a menu it triggers is open)."
29668 },
29669 {
29670 name: "danger",
29671 type: "boolean attribute",
29672 description: "Swaps the hover wash to red — used by the close button."
29673 }
29674 ],
29675 slots: [
29676 { name: "(default)", description: "Optional custom icon markup (inline SVG) when `icon` is omitted." }
29677 ],
29678 cssProps: [
29679 { name: "--wpd-btn-color", description: "Resting foreground." },
29680 { name: "--wpd-btn-color-hover", description: "Hover foreground." },
29681 { name: "--wpd-btn-bg-hover", description: "Hover background wash." },
29682 { name: "--wpd-btn-bg-active", description: "Pressed background." },
29683 { name: "--wpd-btn-danger-hover", description: "Hover background for danger variant." },
29684 { name: "--wpd-btn-outline", description: "Focus outline colour." }
29685 ],
29686 example: html`
29687 <wpd-cluster gap="2">
29688 <wpd-window-button icon="minimize"></wpd-window-button>
29689 <wpd-window-button icon="maximize"></wpd-window-button>
29690 <wpd-window-button icon="menu"></wpd-window-button>
29691 <wpd-window-button icon="close" danger></wpd-window-button>
29692 </wpd-cluster>
29693 `
29694 };
29695 let WpdWindowButton = _WpdWindowButton;
29696 defineComponent("wpd-window-button", WpdWindowButton);
29697 const DEFAULT_STICKY_TITLE = "Sticky Note";
29698 const LEGACY_METADATA_PREFIX = "<!-- wpworkspace-sticky:";
29699 const LEGACY_METADATA_SUFFIX = "-->";
29700 const TITLE_MAX = 64;
29701 const GENERATED_TITLE_MAX = 48;
29702 const EXCERPT_MAX = 180;
29703 function noteFromGuideline(guideline) {
29704 const title = titleField(guideline.title);
29705 const content = removeLegacyMetadataComment(
29706 textFieldValue(guideline.content, { stripHtmlForRendered: true })
29707 );
29708 const modifiedMs = modifiedTimeMs(guideline);
29709 return {
29710 localId: `guideline:${guideline.id}`,
29711 guidelineId: guideline.id,
29712 title,
29713 body: editorBody(title, content),
29714 modified: guideline.modified,
29715 ...modifiedMs > 0 ? { modifiedMs } : {},
29716 link: guideline.link,
29717 termIds: Array.isArray(guideline.wp_guideline_type) ? guideline.wp_guideline_type.filter(isFiniteNumber) : []
29718 };
29719 }
29720 function titleField(field) {
29721 const candidates = [];
29722 if (typeof field === "string") {
29723 candidates.push(field);
29724 } else if (field && typeof field === "object") {
29725 if (typeof field.raw === "string") {
29726 candidates.push(field.raw);
29727 }
29728 if (typeof field.rendered === "string") {
29729 candidates.push(stripHtml(field.rendered));
29730 }
29731 }
29732 for (const candidate of candidates) {
29733 const trimmed = stripHtml(candidate).trim();
29734 if (trimmed) {
29735 return trimmed;
29736 }
29737 }
29738 return DEFAULT_STICKY_TITLE;
29739 }
29740 function textFieldValue(field, options = {}) {
29741 if (typeof field === "string") {
29742 return field;
29743 }
29744 if (!field || typeof field !== "object") {
29745 return "";
29746 }
29747 if (typeof field.raw === "string" && field.raw.length > 0) {
29748 return field.raw;
29749 }
29750 if (typeof field.rendered === "string") {
29751 return options.stripHtmlForRendered ? stripHtml(field.rendered) : field.rendered;
29752 }
29753 return "";
29754 }
29755 function titleForBody(body) {
29756 const line = body.split(/\r?\n/).find((item) => item.trim().length > 0)?.trim();
29757 const title = line && line.length > 0 ? line : DEFAULT_STICKY_TITLE;
29758 return truncate(title, TITLE_MAX);
29759 }
29760 function generatedTitle(body) {
29761 const collapsed = body.replace(/\s+/g, " ").trim();
29762 const title = collapsed || DEFAULT_STICKY_TITLE;
29763 return truncate(title, GENERATED_TITLE_MAX);
29764 }
29765 function editorBody(title, content) {
29766 const trimmedTitle = title.trim();
29767 if (!trimmedTitle) {
29768 return content;
29769 }
29770 const firstLine = content.split(/\r?\n/)[0]?.trim();
29771 if (firstLine === trimmedTitle) {
29772 return content;
29773 }
29774 if (!content) {
29775 return trimmedTitle;
29776 }
29777 return `${trimmedTitle}
29778 ${content}`;
29779 }
29780 function noteComponentsForBody(editorValue, fallbackTitle = DEFAULT_STICKY_TITLE) {
29781 const fallback = fallbackTitle.trim() || DEFAULT_STICKY_TITLE;
29782 const title = titleForBody(editorValue);
29783 const firstNewline = editorValue.search(/\r?\n/);
29784 if (firstNewline === -1) {
29785 const resolvedTitle = title === DEFAULT_STICKY_TITLE ? fallback : title;
29786 return {
29787 title: resolvedTitle,
29788 content: "",
29789 excerpt: excerptFor(resolvedTitle)
29790 };
29791 }
29792 let content = editorValue.slice(firstNewline);
29793 content = content.replace(/^\r?\n/, "");
29794 if (content.startsWith("\n")) {
29795 content = content.slice(1);
29796 }
29797 return {
29798 title,
29799 content,
29800 excerpt: excerptFor(content.trim() ? content : title)
29801 };
29802 }
29803 function excerptFor(body) {
29804 const collapsed = body.replace(/[\n\t]+/g, " ").trim();
29805 return truncate(collapsed, EXCERPT_MAX);
29806 }
29807 function removeLegacyMetadataComment(content) {
29808 if (!content.startsWith(LEGACY_METADATA_PREFIX) || !content.includes(LEGACY_METADATA_SUFFIX)) {
29809 return content;
29810 }
29811 const end = content.indexOf(LEGACY_METADATA_SUFFIX);
29812 let body = content.slice(end + LEGACY_METADATA_SUFFIX.length);
29813 if (body.startsWith("\r\n")) {
29814 body = body.slice(2);
29815 } else if (body.startsWith("\n")) {
29816 body = body.slice(1);
29817 }
29818 return body;
29819 }
29820 function stripHtml(value) {
29821 if (typeof document !== "undefined") {
29822 const template = document.createElement("template");
29823 template.innerHTML = value;
29824 return (template.content.textContent ?? "").trim();
29825 }
29826 return value.replace(/<[^>]*>/g, "").trim();
29827 }
29828 function truncate(value, max) {
29829 return value.length > max ? `${value.slice(0, max)}...` : value;
29830 }
29831 function modifiedTimeMs(guideline) {
29832 if (typeof guideline.desktop_mode_modified_ms === "number" && Number.isFinite(guideline.desktop_mode_modified_ms)) {
29833 return guideline.desktop_mode_modified_ms;
29834 }
29835 if (!guideline.modified) {
29836 return 0;
29837 }
29838 const parsed = Date.parse(guideline.modified);
29839 return Number.isFinite(parsed) ? parsed : 0;
29840 }
29841 function isFiniteNumber(value) {
29842 return typeof value === "number" && Number.isFinite(value);
29843 }
29844 class StickyNotesRestError extends Error {
29845 constructor(message, status) {
29846 super(message);
29847 this.name = "StickyNotesRestError";
29848 this.status = status;
29849 }
29850 }
29851 async function resolveStickyTerms(config) {
29852 const terms = await fetchStickyTermCandidates(config);
29853 const picked = pickStickyTerms(
29854 [...terms.artifactTerms, ...terms.artifactsTerms],
29855 terms.noteTerms,
29856 terms.stickyTerms
29857 );
29858 if (picked) {
29859 return picked;
29860 }
29861 const artifact = await ensureTerm(config, {
29862 slug: "artifact",
29863 name: "Artifact",
29864 parent: 0
29865 });
29866 const note = await ensureTerm(config, {
29867 slug: "note",
29868 name: "Note",
29869 parent: artifact.id
29870 });
29871 const sticky = await ensureTerm(config, {
29872 slug: "sticky",
29873 name: "Sticky",
29874 parent: artifact.id
29875 });
29876 return {
29877 stickyTermId: sticky.id,
29878 termIds: uniqueNumbers([artifact.id, note.id, sticky.id])
29879 };
29880 }
29881 async function fetchStickyTermCandidates(config) {
29882 const [artifactTerms, artifactsTerms, noteTerms, stickyTerms] = await Promise.all([
29883 fetchTermsBySlug(config, "artifact"),
29884 fetchTermsBySlug(config, "artifacts"),
29885 fetchTermsBySlug(config, "note"),
29886 fetchTermsBySlug(config, "sticky")
29887 ]);
29888 return {
29889 artifactTerms,
29890 artifactsTerms,
29891 noteTerms,
29892 stickyTerms
29893 };
29894 }
29895 function pickStickyTerms(artifactTerms, noteTerms, stickyTerms) {
29896 if (stickyTerms.length === 0) {
29897 return null;
29898 }
29899 const artifact = artifactTerms.find(
29900 (term) => ["artifact", "artifacts"].includes(term.slug)
29901 ) ?? artifactTerms[0] ?? null;
29902 const sticky = artifact ? stickyTerms.find((term) => Number(term.parent) === artifact.id) ?? stickyTerms[0] : stickyTerms[0];
29903 if (!sticky) {
29904 return null;
29905 }
29906 const note = artifact ? noteTerms.find((term) => Number(term.parent) === artifact.id) ?? null : null;
29907 return {
29908 stickyTermId: sticky.id,
29909 termIds: uniqueNumbers([
29910 artifact?.id,
29911 note?.id,
29912 sticky.id
29913 ])
29914 };
29915 }
29916 async function fetchStickyNotes(config, stickyTermId) {
29917 const guidelines = await requestJson(
29918 config,
29919 pathWithQuery("wp/v2/guidelines", {
29920 context: "edit",
29921 status: "private",
29922 per_page: "100",
29923 orderby: "modified",
29924 order: "desc",
29925 wp_guideline_type: String(stickyTermId)
29926 }),
29927 void 0,
29928 true
29929 );
29930 return guidelines.filter(
29931 (guideline) => Array.isArray(guideline.wp_guideline_type) ? guideline.wp_guideline_type.includes(stickyTermId) : true
29932 ).map(noteFromGuideline);
29933 }
29934 async function saveStickyNote(config, note, terms) {
29935 const components = noteComponentsForBody(note.body, note.title);
29936 const payload = {
29937 status: "private",
29938 title: components.title,
29939 content: components.content,
29940 excerpt: components.excerpt
29941 };
29942 if (note.guidelineId === null) {
29943 payload.wp_guideline_type = terms.termIds;
29944 }
29945 const path = note.guidelineId === null ? "wp/v2/guidelines" : `wp/v2/guidelines/${note.guidelineId}`;
29946 const guideline = await requestJson(
29947 config,
29948 path,
29949 {
29950 method: "POST",
29951 headers: {
29952 "Content-Type": "application/json"
29953 },
29954 body: JSON.stringify(payload)
29955 },
29956 false
29957 );
29958 return noteFromGuideline(guideline);
29959 }
29960 function buildGuidelineEditUrl(adminUrl, guidelineId) {
29961 const url = new URL("post.php", adminUrl);
29962 url.searchParams.set("post", String(guidelineId));
29963 url.searchParams.set("action", "edit");
29964 return url.toString();
29965 }
29966 async function fetchTermsBySlug(config, slug) {
29967 try {
29968 return await requestJson(
29969 config,
29970 pathWithQuery("wp/v2/wp_guideline_type", {
29971 context: "edit",
29972 slug,
29973 per_page: "100"
29974 }),
29975 void 0,
29976 true
29977 );
29978 } catch (error) {
29979 if (error instanceof StickyNotesRestError && (error.status === 404 || error.status === 400)) {
29980 return [];
29981 }
29982 throw error;
29983 }
29984 }
29985 async function ensureTerm(config, term) {
29986 const existing = await fetchTermsBySlug(config, term.slug);
29987 const byParent = existing.find(
29988 (item) => Number(item.parent ?? 0) === term.parent
29989 );
29990 if (byParent) {
29991 return byParent;
29992 }
29993 if (existing[0]) {
29994 return existing[0];
29995 }
29996 try {
29997 return await requestJson(
29998 config,
29999 "wp/v2/wp_guideline_type",
30000 {
30001 method: "POST",
30002 headers: {
30003 "Content-Type": "application/json"
30004 },
30005 body: JSON.stringify(term)
30006 },
30007 true
30008 );
30009 } catch (error) {
30010 const fallback = await fetchTermsBySlug(config, term.slug);
30011 if (fallback[0]) {
30012 return fallback[0];
30013 }
30014 throw error;
30015 }
30016 }
30017 async function requestJson(config, path, init2, silent = true) {
30018 const response = await trackedFetch$1(
30019 joinRestUrl(restRoot(config), path),
30020 init2,
30021 {
30022 source: "desktop-mode/sticky-notes",
30023 silent
30024 }
30025 );
30026 if (!response.ok) {
30027 throw new StickyNotesRestError(
30028 response.statusText || `${DEFAULT_STICKY_TITLE} request failed`,
30029 response.status
30030 );
30031 }
30032 return await response.json();
30033 }
30034 function restRoot(config) {
30035 if (config.restUrl) {
30036 return config.restUrl;
30037 }
30038 return `${window.location.origin}/wp-json/`;
30039 }
30040 function pathWithQuery(path, query) {
30041 const params = new URLSearchParams();
30042 Object.entries(query).forEach(([key, value]) => {
30043 params.set(key, value);
30044 });
30045 return `${path}?${params.toString()}`;
30046 }
30047 function uniqueNumbers(values) {
30048 const out = [];
30049 values.forEach((value) => {
30050 if (typeof value === "number" && Number.isFinite(value) && !out.includes(value)) {
30051 out.push(value);
30052 }
30053 });
30054 return out;
30055 }
30056 const SUBSCRIBE_FIELD$1 = "desktop_mode_sticky_notes_subscribe";
30057 const RESPONSE_FIELD$1 = "desktop_mode_sticky_notes";
30058 let started$4 = false;
30059 let target$1 = null;
30060 function startStickyNotesHeartbeat(nextTarget) {
30061 target$1 = nextTarget;
30062 if (started$4) {
30063 return;
30064 }
30065 started$4 = true;
30066 heartbeat.contribute(
30067 SUBSCRIBE_FIELD$1,
30068 () => target$1?.getHeartbeatSubscription()
30069 );
30070 heartbeat.subscribe(
30071 RESPONSE_FIELD$1,
30072 (payload) => {
30073 target$1?.applyHeartbeatPayload(payload);
30074 }
30075 );
30076 }
30077 const GEOMETRY_KEY = "desktop-mode-sticky-notes-geometry";
30078 const DEFAULT_WIDTH = 264;
30079 const DEFAULT_HEIGHT = 176;
30080 const MIN_WIDTH = 180;
30081 const MIN_HEIGHT = 128;
30082 const EDGE_PADDING = 16;
30083 const SAVE_DEBOUNCE_MS$1 = 1e3;
30084 class StickyNotesLayer {
30085 constructor(options) {
30086 this.root = null;
30087 this.terms = null;
30088 this.controllers = /* @__PURE__ */ new Map();
30089 this.contextMenuInstalled = false;
30090 this.desktopHooksInstalled = false;
30091 this.highWaterMs = 0;
30092 this.zIndexCounter = 0;
30093 this.host = options.host;
30094 this.config = options.config;
30095 this.available = options.available ?? true;
30096 this.openArtifact = options.openArtifact;
30097 this.getActiveDesktopId = options.getActiveDesktopId ?? (() => "desktop-1");
30098 this.onError = options.onError;
30099 }
30100 async boot() {
30101 if (!this.available) {
30102 return;
30103 }
30104 try {
30105 this.terms = await resolveStickyTerms(this.config);
30106 if (!this.terms) {
30107 return;
30108 }
30109 this.installContextMenu();
30110 this.installDesktopHooks();
30111 const notes = await fetchStickyNotes(
30112 this.config,
30113 this.terms.stickyTermId
30114 );
30115 this.bumpHighWaterFromNotes(notes);
30116 startStickyNotesHeartbeat(this);
30117 if (notes.length === 0) {
30118 return;
30119 }
30120 this.ensureRoot();
30121 sortNotesByModified(notes).forEach(
30122 (note, index2) => this.upsert(note, index2)
30123 );
30124 } catch (error) {
30125 if (error instanceof Error) {
30126 console.debug("[desktop-mode] Sticky notes unavailable:", error.message);
30127 }
30128 }
30129 }
30130 createNote(body = "") {
30131 if (!this.terms) {
30132 return;
30133 }
30134 const note = {
30135 localId: `local:${Date.now()}:${Math.random().toString(36).slice(2)}`,
30136 guidelineId: null,
30137 title: body.trim() ? generatedTitle(body) : DEFAULT_STICKY_TITLE,
30138 body,
30139 termIds: this.terms.termIds
30140 };
30141 const controller = this.upsert(note, this.controllers.size, {
30142 activate: true
30143 });
30144 controller.focus();
30145 }
30146 upsert(note, index2, options = {}) {
30147 this.ensureRoot();
30148 const key = noteKey(note);
30149 const existing = this.controllers.get(key);
30150 if (existing) {
30151 existing.replace(note);
30152 if (options.activate) {
30153 this.bringToFront(existing);
30154 }
30155 return existing;
30156 }
30157 const controller = new StickyNoteController({
30158 layer: this,
30159 note,
30160 index: index2
30161 });
30162 this.controllers.set(key, controller);
30163 this.root?.appendChild(controller.element);
30164 this.assignZIndex(controller);
30165 this.applyDesktopVisibility(controller);
30166 if (options.activate) {
30167 this.bringToFront(controller);
30168 }
30169 return controller;
30170 }
30171 ensureRoot() {
30172 if (this.root) {
30173 return this.root;
30174 }
30175 const root = document.createElement("section");
30176 root.className = "desktop-mode-sticky-notes";
30177 root.setAttribute("aria-label", __("Sticky notes"));
30178 this.host.appendChild(root);
30179 this.root = root;
30180 return root;
30181 }
30182 installContextMenu() {
30183 if (this.contextMenuInstalled) {
30184 return;
30185 }
30186 this.contextMenuInstalled = true;
30187 addFilter(
30188 "desktop-mode.wallpaper-context-menu",
30189 "desktop-mode/sticky-notes",
30190 (items) => {
30191 if (!Array.isArray(items) || !this.terms) {
30192 return items;
30193 }
30194 if (items.some(
30195 (item) => item.id === "new-sticky-note"
30196 )) {
30197 return items;
30198 }
30199 return [
30200 ...items,
30201 {
30202 id: "new-sticky-note",
30203 label: __("New sticky note"),
30204 icon: "dashicons-edit-page",
30205 sort: 14,
30206 onClick: () => this.createNote()
30207 }
30208 ];
30209 }
30210 );
30211 }
30212 installDesktopHooks() {
30213 if (this.desktopHooksInstalled) {
30214 return;
30215 }
30216 this.desktopHooksInstalled = true;
30217 addAction(
30218 HOOKS.DESKTOP_SWITCHED,
30219 "desktop-mode/sticky-notes",
30220 () => this.refreshDesktopVisibility()
30221 );
30222 addAction(
30223 HOOKS.DESKTOP_CLOSED,
30224 "desktop-mode/sticky-notes",
30225 (detail) => {
30226 this.migrateDesktopAssignments(detail?.desktopId, detail?.migratedTo);
30227 this.refreshDesktopVisibility();
30228 }
30229 );
30230 }
30231 save(note) {
30232 if (!this.terms) {
30233 return Promise.reject(new Error(__("Sticky term is unavailable.")));
30234 }
30235 return saveStickyNote(this.config, note, this.terms);
30236 }
30237 getHeartbeatSubscription() {
30238 if (!this.terms) {
30239 return void 0;
30240 }
30241 return {
30242 stickyTermId: this.terms.stickyTermId,
30243 knownIds: this.knownGuidelineIds(),
30244 version: this.highWaterMs
30245 };
30246 }
30247 applyHeartbeatPayload(payload) {
30248 for (const guideline of payload.notes ?? []) {
30249 const note = noteFromGuideline(guideline);
30250 this.upsertRemote(note);
30251 }
30252 for (const id of payload.removed ?? []) {
30253 this.forgetGuidelineId(id);
30254 }
30255 if (typeof payload.serverTimeMs === "number" && Number.isFinite(payload.serverTimeMs) && payload.serverTimeMs > this.highWaterMs) {
30256 this.highWaterMs = payload.serverTimeMs;
30257 }
30258 if (payload.truncated) {
30259 void this.reloadFromServer();
30260 }
30261 }
30262 openNoteArtifact(note) {
30263 if (note.guidelineId === null) {
30264 return;
30265 }
30266 this.openArtifact(
30267 buildGuidelineEditUrl(this.config.adminUrl, note.guidelineId),
30268 note.title,
30269 note.guidelineId
30270 );
30271 }
30272 notifyError(message) {
30273 this.onError?.(message);
30274 }
30275 hostSize() {
30276 return {
30277 width: Math.max(1, this.host.clientWidth),
30278 height: Math.max(1, this.host.clientHeight)
30279 };
30280 }
30281 defaultGeometry(index2) {
30282 const { width: hostWidth, height: hostHeight } = this.hostSize();
30283 const width = Math.min(
30284 DEFAULT_WIDTH,
30285 Math.max(MIN_WIDTH, hostWidth - EDGE_PADDING * 2)
30286 );
30287 const height = Math.min(
30288 DEFAULT_HEIGHT,
30289 Math.max(MIN_HEIGHT, hostHeight - EDGE_PADDING * 2)
30290 );
30291 const offset = index2 % 8 * 28;
30292 const left = clamp(
30293 hostWidth - width - 32 - offset,
30294 EDGE_PADDING,
30295 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
30296 );
30297 const top = clamp(
30298 32 + offset,
30299 EDGE_PADDING,
30300 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
30301 );
30302 return {
30303 x: left / hostWidth,
30304 y: top / hostHeight,
30305 width,
30306 height
30307 };
30308 }
30309 forget(controller) {
30310 this.controllers.delete(noteKey(controller.note));
30311 controller.dispose();
30312 controller.element.remove();
30313 if (this.controllers.size === 0) {
30314 this.root?.remove();
30315 this.root = null;
30316 }
30317 }
30318 replaceControllerKey(oldKey, controller) {
30319 const newKey = noteKey(controller.note);
30320 this.controllers.delete(oldKey);
30321 this.controllers.set(newKey, controller);
30322 moveStoredGeometry(oldKey, newKey);
30323 this.applyDesktopVisibility(controller);
30324 }
30325 bumpHighWaterFromNote(note) {
30326 const modifiedMs = noteModifiedMs(note);
30327 if (modifiedMs > this.highWaterMs) {
30328 this.highWaterMs = modifiedMs;
30329 }
30330 }
30331 bringToFront(controller) {
30332 controller.setZIndex(this.nextZIndex());
30333 }
30334 geometryForNote(note, index2) {
30335 const key = noteKey(note);
30336 const loaded = loadGeometry(key);
30337 const desktopId = this.normalizeDesktopId(loaded?.desktopId);
30338 const geometry = loaded ? { ...loaded, desktopId } : { ...this.defaultGeometry(index2), desktopId };
30339 if (!loaded || loaded.desktopId !== geometry.desktopId) {
30340 saveGeometry(key, geometry);
30341 }
30342 return geometry;
30343 }
30344 upsertRemote(note) {
30345 const key = noteKey(note);
30346 const existing = this.controllers.get(key);
30347 if (existing) {
30348 if (!existing.shouldReplaceFromRemote(note)) {
30349 this.bumpHighWaterFromNote(note);
30350 return existing;
30351 }
30352 existing.replace(note);
30353 this.bumpHighWaterFromNote(note);
30354 return existing;
30355 }
30356 const controller = this.upsert(note, this.controllers.size);
30357 this.bumpHighWaterFromNote(note);
30358 return controller;
30359 }
30360 forgetGuidelineId(guidelineId) {
30361 for (const controller of this.controllers.values()) {
30362 if (controller.note.guidelineId === guidelineId) {
30363 this.forget(controller);
30364 return;
30365 }
30366 }
30367 }
30368 knownGuidelineIds() {
30369 const ids = [];
30370 for (const controller of this.controllers.values()) {
30371 if (controller.note.guidelineId !== null) {
30372 ids.push(controller.note.guidelineId);
30373 }
30374 }
30375 return ids;
30376 }
30377 bumpHighWaterFromNotes(notes) {
30378 notes.forEach((note) => this.bumpHighWaterFromNote(note));
30379 }
30380 assignZIndex(controller) {
30381 controller.setZIndex(this.nextZIndex());
30382 }
30383 nextZIndex() {
30384 this.zIndexCounter += 1;
30385 return this.zIndexCounter;
30386 }
30387 applyDesktopVisibility(controller) {
30388 controller.setVisible(this.isNoteOnActiveDesktop(controller.note));
30389 }
30390 refreshDesktopVisibility() {
30391 for (const controller of this.controllers.values()) {
30392 this.applyDesktopVisibility(controller);
30393 }
30394 }
30395 isNoteOnActiveDesktop(note) {
30396 const key = noteKey(note);
30397 const geometry = loadGeometry(key);
30398 const desktopId = this.normalizeDesktopId(geometry?.desktopId);
30399 if (geometry && geometry.desktopId !== desktopId) {
30400 saveGeometry(key, { ...geometry, desktopId });
30401 }
30402 return desktopId === this.activeDesktopId();
30403 }
30404 migrateDesktopAssignments(desktopId, migratedTo) {
30405 if (!desktopId || !migratedTo || desktopId === migratedTo) {
30406 return;
30407 }
30408 const map = readGeometryMap();
30409 let changed = false;
30410 Object.entries(map).forEach(([key, geometry]) => {
30411 if (geometry.desktopId === desktopId) {
30412 map[key] = {
30413 ...geometry,
30414 desktopId: this.normalizeDesktopId(migratedTo)
30415 };
30416 changed = true;
30417 }
30418 });
30419 if (changed) {
30420 writeGeometryMap(map);
30421 }
30422 }
30423 activeDesktopId() {
30424 try {
30425 const id = this.getActiveDesktopId();
30426 return typeof id === "string" && id ? id : "desktop-1";
30427 } catch {
30428 return "desktop-1";
30429 }
30430 }
30431 normalizeDesktopId(desktopId) {
30432 if (!desktopId) {
30433 return this.activeDesktopId();
30434 }
30435 return desktopId;
30436 }
30437 async reloadFromServer() {
30438 if (!this.terms) {
30439 return;
30440 }
30441 try {
30442 const notes = await fetchStickyNotes(
30443 this.config,
30444 this.terms.stickyTermId
30445 );
30446 const ids = /* @__PURE__ */ new Set();
30447 sortNotesByModified(notes).forEach((note) => {
30448 if (note.guidelineId !== null) {
30449 ids.add(note.guidelineId);
30450 }
30451 this.upsertRemote(note);
30452 });
30453 this.knownGuidelineIds().forEach((id) => {
30454 if (!ids.has(id)) {
30455 this.forgetGuidelineId(id);
30456 }
30457 });
30458 } catch {
30459 }
30460 }
30461 }
30462 class StickyNoteController {
30463 constructor(options) {
30464 this.saveTimer = null;
30465 this.geometryTimer = null;
30466 this.saving = false;
30467 this.saveAgain = false;
30468 this.resizeObserver = null;
30469 this.disposed = false;
30470 this.layer = options.layer;
30471 this.note = options.note;
30472 this.index = options.index;
30473 this.element = document.createElement("article");
30474 this.element.className = "desktop-mode-sticky-note";
30475 this.element.dataset.stickyNoteId = noteKey(this.note);
30476 this.titleEl = document.createElement("span");
30477 this.editor = document.createElement("wpd-textarea");
30478 this.statusEl = document.createElement("wpd-save-status");
30479 this.openButton = document.createElement("wpd-window-button");
30480 this.paint();
30481 this.applyGeometry(this.layer.geometryForNote(this.note, this.index));
30482 this.element.addEventListener(
30483 "pointerdown",
30484 () => this.layer.bringToFront(this),
30485 { capture: true }
30486 );
30487 this.element.addEventListener("focusin", () => this.layer.bringToFront(this));
30488 this.watchResize();
30489 }
30490 focus() {
30491 window.setTimeout(() => this.editor.focusInput?.(), 0);
30492 }
30493 replace(note) {
30494 this.note = note;
30495 this.element.dataset.stickyNoteId = noteKey(this.note);
30496 this.titleEl.textContent = this.note.title;
30497 this.editor.setAttribute("value", this.note.body);
30498 this.refreshOpenButton();
30499 }
30500 shouldReplaceFromRemote(note) {
30501 if (this.hasLocalChanges()) {
30502 return false;
30503 }
30504 const currentMs = noteModifiedMs(this.note);
30505 const incomingMs = noteModifiedMs(note);
30506 if (currentMs > 0 && incomingMs > 0 && incomingMs <= currentMs && this.note.title === note.title && this.note.body === note.body) {
30507 return false;
30508 }
30509 return true;
30510 }
30511 setZIndex(zIndex) {
30512 this.element.style.zIndex = String(zIndex);
30513 }
30514 setVisible(visible) {
30515 this.element.style.display = visible ? "" : "none";
30516 }
30517 dispose() {
30518 this.disposed = true;
30519 if (this.saveTimer !== null) {
30520 window.clearTimeout(this.saveTimer);
30521 this.saveTimer = null;
30522 }
30523 if (this.geometryTimer !== null) {
30524 window.clearTimeout(this.geometryTimer);
30525 this.geometryTimer = null;
30526 }
30527 this.resizeObserver?.disconnect();
30528 this.resizeObserver = null;
30529 }
30530 paint() {
30531 this.element.innerHTML = "";
30532 this.element.style.minWidth = `${MIN_WIDTH}px`;
30533 this.element.style.minHeight = `${MIN_HEIGHT}px`;
30534 const header = document.createElement("div");
30535 header.className = "desktop-mode-sticky-note__header";
30536 const grip = document.createElement("span");
30537 grip.className = "desktop-mode-sticky-note__grip";
30538 grip.setAttribute("aria-hidden", "true");
30539 this.titleEl.className = "desktop-mode-sticky-note__title";
30540 this.titleEl.textContent = this.note.title;
30541 this.statusEl.setAttribute("mode", "icon");
30542 this.statusEl.setAttribute("phase", "idle");
30543 this.statusEl.className = "desktop-mode-sticky-note__status";
30544 this.openButton.setAttribute("icon", "detach");
30545 this.openButton.setAttribute("title", __("Open artifact"));
30546 this.openButton.className = "desktop-mode-sticky-note__open";
30547 this.openButton.addEventListener("wpd-button-activate", () => {
30548 this.layer.openNoteArtifact(this.note);
30549 });
30550 const close = document.createElement("wpd-window-button");
30551 close.setAttribute("icon", "close");
30552 close.setAttribute("danger", "");
30553 close.setAttribute("title", __("Hide sticky note"));
30554 close.className = "desktop-mode-sticky-note__close";
30555 close.addEventListener("wpd-button-activate", () => this.close());
30556 header.append(grip, this.titleEl, this.statusEl, this.openButton, close);
30557 header.addEventListener("pointerdown", (event) => this.startDrag(event));
30558 this.editor.className = "desktop-mode-sticky-note__editor";
30559 this.editor.setAttribute("aria-label", __("Sticky note text"));
30560 this.editor.setAttribute("rows", "8");
30561 this.editor.setAttribute("value", this.note.body);
30562 this.installEditorKeyboardGuard();
30563 this.editor.addEventListener("wpd-input-change", (event) => {
30564 const detail = event.detail;
30565 this.note.body = detail.value;
30566 this.note.title = titleForBody(detail.value);
30567 this.titleEl.textContent = this.note.title;
30568 this.setPhase("pending");
30569 this.scheduleSave();
30570 });
30571 this.editor.addEventListener("wpd-input-commit", () => this.flushSave());
30572 this.element.append(header, this.editor);
30573 this.refreshOpenButton();
30574 }
30575 installEditorKeyboardGuard() {
30576 ["keydown", "keypress", "keyup"].forEach((eventName) => {
30577 this.editor.addEventListener(eventName, (event) => {
30578 event.stopPropagation();
30579 });
30580 });
30581 }
30582 refreshOpenButton() {
30583 const disabled = this.note.guidelineId === null;
30584 this.openButton.classList.toggle("is-disabled", disabled);
30585 this.openButton.setAttribute("aria-disabled", disabled ? "true" : "false");
30586 }
30587 close() {
30588 if (this.note.guidelineId === null && this.note.body.trim().length === 0) {
30589 this.layer.forget(this);
30590 return;
30591 }
30592 this.flushSave();
30593 this.layer.forget(this);
30594 }
30595 scheduleSave() {
30596 if (this.note.guidelineId === null && this.note.body.trim().length === 0) {
30597 this.setPhase("idle");
30598 return;
30599 }
30600 if (this.saveTimer !== null) {
30601 window.clearTimeout(this.saveTimer);
30602 }
30603 this.saveTimer = window.setTimeout(() => {
30604 this.saveTimer = null;
30605 void this.save();
30606 }, SAVE_DEBOUNCE_MS$1);
30607 }
30608 flushSave() {
30609 if (this.saveTimer !== null) {
30610 window.clearTimeout(this.saveTimer);
30611 this.saveTimer = null;
30612 }
30613 if (this.note.guidelineId !== null || this.note.body.trim().length > 0) {
30614 void this.save();
30615 }
30616 }
30617 async save() {
30618 if (this.saving) {
30619 this.saveAgain = true;
30620 this.setPhase("pending");
30621 return;
30622 }
30623 this.saving = true;
30624 this.setPhase("saving");
30625 const bodyAtSave = this.note.body;
30626 try {
30627 const saved = await this.layer.save({
30628 ...this.note,
30629 body: bodyAtSave
30630 });
30631 if (this.disposed) {
30632 return;
30633 }
30634 const oldKey = noteKey(this.note);
30635 this.note.guidelineId = saved.guidelineId;
30636 this.note.modified = saved.modified;
30637 this.note.link = saved.link;
30638 this.note.termIds = saved.termIds.length > 0 ? saved.termIds : this.note.termIds;
30639 if (this.note.body === bodyAtSave) {
30640 this.note.title = saved.title;
30641 this.titleEl.textContent = saved.title;
30642 }
30643 if (oldKey !== noteKey(this.note)) {
30644 this.element.dataset.stickyNoteId = noteKey(this.note);
30645 this.layer.replaceControllerKey(oldKey, this);
30646 }
30647 this.layer.bumpHighWaterFromNote(this.note);
30648 this.refreshOpenButton();
30649 this.setPhase("saved");
30650 } catch (error) {
30651 if (this.disposed) {
30652 return;
30653 }
30654 const message = error instanceof Error ? error.message : __("Could not save sticky note.");
30655 this.setPhase("failed", message);
30656 this.layer.notifyError(message);
30657 } finally {
30658 this.saving = false;
30659 if (!this.disposed && this.saveAgain) {
30660 this.saveAgain = false;
30661 this.scheduleSave();
30662 }
30663 }
30664 }
30665 setPhase(phase, error) {
30666 this.statusEl.setAttribute("phase", phase);
30667 if (error) {
30668 this.statusEl.setAttribute("error", error);
30669 this.statusEl.setAttribute("title", error);
30670 } else {
30671 this.statusEl.removeAttribute("error");
30672 this.statusEl.removeAttribute("title");
30673 }
30674 }
30675 hasLocalChanges() {
30676 const phase = this.statusEl.getAttribute("phase");
30677 return this.saveTimer !== null || this.saving || this.saveAgain || phase === "pending" || phase === "failed";
30678 }
30679 startDrag(event) {
30680 if (event.button !== 0) {
30681 return;
30682 }
30683 const target2 = event.target;
30684 if (target2?.closest("wpd-window-button, wpd-save-status")) {
30685 return;
30686 }
30687 event.preventDefault();
30688 const startRect = this.element.getBoundingClientRect();
30689 const hostRect = this.layerHostRect();
30690 const startLeft = startRect.left - hostRect.left;
30691 const startTop = startRect.top - hostRect.top;
30692 const startX = event.clientX;
30693 const startY = event.clientY;
30694 this.element.classList.add("desktop-mode-sticky-note--dragging");
30695 this.element.setPointerCapture?.(event.pointerId);
30696 const move = (moveEvent) => {
30697 const width = this.element.offsetWidth;
30698 const height = this.element.offsetHeight;
30699 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
30700 const left = clamp(
30701 startLeft + moveEvent.clientX - startX,
30702 EDGE_PADDING,
30703 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
30704 );
30705 const top = clamp(
30706 startTop + moveEvent.clientY - startY,
30707 EDGE_PADDING,
30708 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
30709 );
30710 this.element.style.left = `${left}px`;
30711 this.element.style.top = `${top}px`;
30712 };
30713 const up = (upEvent) => {
30714 this.element.classList.remove("desktop-mode-sticky-note--dragging");
30715 this.element.releasePointerCapture?.(upEvent.pointerId);
30716 document.removeEventListener("pointermove", move);
30717 document.removeEventListener("pointerup", up);
30718 this.persistGeometry();
30719 };
30720 document.addEventListener("pointermove", move);
30721 document.addEventListener("pointerup", up);
30722 }
30723 applyGeometry(geometry) {
30724 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
30725 const width = clamp(geometry.width, MIN_WIDTH, hostWidth - EDGE_PADDING * 2);
30726 const height = clamp(geometry.height, MIN_HEIGHT, hostHeight - EDGE_PADDING * 2);
30727 const left = clamp(
30728 geometry.x * hostWidth,
30729 EDGE_PADDING,
30730 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
30731 );
30732 const top = clamp(
30733 geometry.y * hostHeight,
30734 EDGE_PADDING,
30735 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
30736 );
30737 this.element.style.left = `${left}px`;
30738 this.element.style.top = `${top}px`;
30739 this.element.style.width = `${width}px`;
30740 this.element.style.height = `${height}px`;
30741 }
30742 watchResize() {
30743 if (typeof ResizeObserver === "undefined") {
30744 return;
30745 }
30746 this.resizeObserver = new ResizeObserver(() => {
30747 if (this.geometryTimer !== null) {
30748 window.clearTimeout(this.geometryTimer);
30749 }
30750 this.geometryTimer = window.setTimeout(() => {
30751 this.geometryTimer = null;
30752 this.persistGeometry();
30753 }, 150);
30754 });
30755 this.resizeObserver.observe(this.element);
30756 }
30757 persistGeometry() {
30758 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
30759 const left = parseFloat(this.element.style.left) || 0;
30760 const top = parseFloat(this.element.style.top) || 0;
30761 const existing = loadGeometry(noteKey(this.note));
30762 saveGeometry(noteKey(this.note), {
30763 ...existing ?? {},
30764 x: clamp(left / hostWidth, 0, 1),
30765 y: clamp(top / hostHeight, 0, 1),
30766 width: this.element.offsetWidth,
30767 height: this.element.offsetHeight
30768 });
30769 }
30770 layerHostRect() {
30771 const parent = this.element.parentElement?.parentElement;
30772 return (parent ?? document.body).getBoundingClientRect();
30773 }
30774 }
30775 function bootStickyNotes(options) {
30776 const layer = new StickyNotesLayer(options);
30777 void layer.boot();
30778 return layer;
30779 }
30780 function noteKey(note) {
30781 return note.guidelineId === null ? note.localId : `guideline:${note.guidelineId}`;
30782 }
30783 function noteModifiedMs(note) {
30784 if (typeof note.modifiedMs === "number" && Number.isFinite(note.modifiedMs)) {
30785 return note.modifiedMs;
30786 }
30787 if (!note.modified) {
30788 return 0;
30789 }
30790 const parsed = Date.parse(note.modified);
30791 return Number.isFinite(parsed) ? parsed : 0;
30792 }
30793 function sortNotesByModified(notes) {
30794 return [...notes].sort((a, b) => noteModifiedMs(a) - noteModifiedMs(b));
30795 }
30796 function loadGeometry(key) {
30797 const map = readGeometryMap();
30798 const value = map[key];
30799 if (!value || !Number.isFinite(value.x) || !Number.isFinite(value.y) || !Number.isFinite(value.width) || !Number.isFinite(value.height)) {
30800 return null;
30801 }
30802 return value;
30803 }
30804 function saveGeometry(key, geometry) {
30805 const map = readGeometryMap();
30806 map[key] = geometry;
30807 writeGeometryMap(map);
30808 }
30809 function moveStoredGeometry(oldKey, newKey) {
30810 if (oldKey === newKey) {
30811 return;
30812 }
30813 const map = readGeometryMap();
30814 if (map[oldKey]) {
30815 map[newKey] = map[oldKey];
30816 delete map[oldKey];
30817 writeGeometryMap(map);
30818 }
30819 }
30820 function readGeometryMap() {
30821 try {
30822 const raw = window.localStorage.getItem(GEOMETRY_KEY);
30823 return raw ? JSON.parse(raw) : {};
30824 } catch {
30825 return {};
30826 }
30827 }
30828 function writeGeometryMap(map) {
30829 try {
30830 window.localStorage.setItem(GEOMETRY_KEY, JSON.stringify(map));
30831 } catch {
30832 }
30833 }
30834 function clamp(value, min, max) {
30835 if (max < min) {
30836 return min;
30837 }
30838 return Math.min(max, Math.max(min, value));
30839 }
30840 const avatarStyles = css`:host{display:inline-flex;position:relative;width:var( --wpd-avatar-size,32px );height:var( --wpd-avatar-size,32px );flex:0 0 auto;vertical-align:middle;line-height:0;perspective:calc( var( --wpd-avatar-size,32px ) * 8 );--wpd-avatar-tilt-x:0deg;--wpd-avatar-tilt-y:0deg;--wpd-avatar-hover:0;--wpd-avatar-glare-x:50%;--wpd-avatar-glare-y:50%}:host( [ hidden ] ){display:none}.wpd-avatar__tile{position:relative;width:100%;height:100%;border-radius:50%;overflow:hidden;background:var( --desktop-mode-window-bg,#f0f0f1 );color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:calc( var( --wpd-avatar-size,32px ) * 0.48 );line-height:1;letter-spacing:0;font-feature-settings:'tnum' 1;user-select:none;transform-style:preserve-3d;transform:rotateX( var( --wpd-avatar-tilt-x ) ) rotateY( var( --wpd-avatar-tilt-y ) ) scale( calc( 1 + var( --wpd-avatar-hover ) * 0.07 ) );transition:transform 220ms cubic-bezier( 0.2,0.8,0.2,1 ),box-shadow 220ms cubic-bezier( 0.2,0.8,0.2,1 );box-shadow:inset 0 0 0 1px rgba( 255,255,255,calc( 0.18 + 0.22 * var( --wpd-avatar-hover ) ) ),inset 0 0 0 calc( 1px + var( --wpd-avatar-hover ) * 1px ) rgba( 0,0,0,calc( 0.08 + 0.04 * var( --wpd-avatar-hover ) ) ),0 calc( 1px + var( --wpd-avatar-hover ) * 8px ) calc( 6px + var( --wpd-avatar-hover ) * 18px ) rgba( 0,0,0,calc( 0.08 + 0.18 * var( --wpd-avatar-hover ) ) )}.wpd-avatar__tile::after{content:'';position:absolute;inset:0;border-radius:50%;background:radial-gradient( circle at var( --wpd-avatar-glare-x ) var( --wpd-avatar-glare-y ),rgba( 255,255,255,0.55 ) 0%,rgba( 255,255,255,0 ) 55% );opacity:var( --wpd-avatar-hover );mix-blend-mode:overlay;pointer-events:none;transition:opacity 220ms cubic-bezier( 0.2,0.8,0.2,1 )}.wpd-avatar__tile::before{content:'';position:absolute;inset:calc( var( --wpd-avatar-hover ) * -3px );border-radius:50%;background:radial-gradient( circle at var( --wpd-avatar-glare-x ) var( --wpd-avatar-glare-y ),rgba( 99,102,241,calc( 0.35 * var( --wpd-avatar-hover ) ) ) 0%,rgba( 99,102,241,0 ) 70% );filter:blur( 4px );pointer-events:none;z-index:-1;transition:inset 220ms cubic-bezier( 0.2,0.8,0.2,1 ),background 220ms}.wpd-avatar__tile img{width:100%;height:100%;object-fit:cover;display:block;transform:translateZ( 1px )}.wpd-avatar__dot{position:absolute;bottom:0;inset-inline-end:0;width:calc( var( --wpd-avatar-size,32px ) * 0.32 );height:calc( var( --wpd-avatar-size,32px ) * 0.32 );min-width:8px;min-height:8px;border-radius:50%;box-sizing:border-box;border:2px solid var( --wpd-avatar-dot-ring,var( --desktop-mode-window-bg,#fff ) );background:var( --wpd-avatar-dot-color,transparent );z-index:2}.wpd-avatar__dot--online{background:var( --desktop-mode-success,#00a32a )}.wpd-avatar__dot--inactive{background:var( --desktop-mode-warning,#dba617 )}.wpd-avatar__dot--offline{background:var( --desktop-mode-muted,#8c8f94 )}@media ( prefers-reduced-motion:reduce ){.wpd-avatar__tile{transform:none;transition:box-shadow 200ms}.wpd-avatar__tile::after,.wpd-avatar__tile::before{display:none}}`;
30841 const SIZE_MAP = {
30842 xs: 20,
30843 sm: 24,
30844 md: 40,
30845 lg: 64,
30846 xl: 96
30847 };
30848 const VALID_PRESENCE = /* @__PURE__ */ new Set(["online", "inactive", "offline"]);
30849 const _WpdAvatar = class _WpdAvatar extends Component {
30850 constructor() {
30851 super(...arguments);
30852 this._presenceHandler = null;
30853 this._imgFailed = false;
30854 this._onPointerMove = null;
30855 this._onPointerEnter = null;
30856 this._onPointerLeave = null;
30857 this._tiltRaf = 0;
30858 this._pendingTiltX = "0deg";
30859 this._pendingTiltY = "0deg";
30860 this._pendingGlareX = "50%";
30861 this._pendingGlareY = "50%";
30862 }
30863 connectedCallback() {
30864 super.connectedCallback();
30865 this._maybeAttachPresenceListener();
30866 this._attachHoverEffect();
30867 }
30868 disconnectedCallback() {
30869 if (this._presenceHandler) {
30870 document.removeEventListener(
30871 "desktop-mode-presence-changed",
30872 this._presenceHandler
30873 );
30874 this._presenceHandler = null;
30875 }
30876 this._detachHoverEffect();
30877 }
30878 attributeChangedCallback(name, oldValue, newValue) {
30879 super.attributeChangedCallback(name, oldValue, newValue);
30880 if (name === "src") {
30881 this._imgFailed = false;
30882 }
30883 if (name === "user-id" || name === "presence") {
30884 this._maybeAttachPresenceListener();
30885 }
30886 }
30887 render() {
30888 const src = this._attr("src");
30889 const name = this._attr("name") || "";
30890 const altRaw = this._attr("alt");
30891 const alt = altRaw !== null ? altRaw : name;
30892 const sizeRaw = this._attr("size");
30893 const size = this._resolveSize(sizeRaw);
30894 const presence = this._presenceForRender();
30895 const clickable = this._attr("clickable") !== null;
30896 this.style.setProperty("--wpd-avatar-size", `${size}px`);
30897 const initialsBg = src && !this._imgFailed ? "" : this._initialsBg(name);
30898 const inner = src && !this._imgFailed ? html`<img
30899 src=${src}
30900 alt=${alt}
30901 @error=${() => this._onImgError()}
30902 loading="lazy"
30903 />` : this._initials(name);
30904 const dot = presence ? html`<span
30905 class=${`wpd-avatar__dot wpd-avatar__dot--${presence}`}
30906 aria-label=${this._presenceLabel(presence)}
30907 ></span>` : html``;
30908 if (clickable) {
30909 return html`
30910 <button
30911 type="button"
30912 class="wpd-avatar__tile"
30913 aria-label=${alt || "User"}
30914 style=${initialsBg ? `background:${initialsBg};` : ""}
30915 @click=${(e) => this._onClick(e)}
30916 >${inner}</button>
30917 ${dot}
30918 `;
30919 }
30920 return html`
30921 <div
30922 class="wpd-avatar__tile"
30923 role="img"
30924 aria-label=${alt || "User"}
30925 style=${initialsBg ? `background:${initialsBg};` : ""}
30926 >${inner}</div>
30927 ${dot}
30928 `;
30929 }
30930 _attr(name) {
30931 return this.getAttribute(name);
30932 }
30933 _resolveSize(raw) {
30934 if (!raw) {
30935 return 32;
30936 }
30937 if (raw in SIZE_MAP) {
30938 return SIZE_MAP[raw];
30939 }
30940 const n = Number(raw);
30941 return Number.isFinite(n) && n > 0 ? n : 32;
30942 }
30943 _initials(name) {
30944 const trimmed = name.trim();
30945 if (!trimmed) {
30946 return "?";
30947 }
30948 return Array.from(trimmed)[0]?.toUpperCase() ?? "?";
30949 }
30950 _initialsBg(name) {
30951 const hue = hashTitleToHue(name);
30952 return `linear-gradient(135deg, hsl(${hue} 62% 55%), hsl(${(hue + 24) % 360} 58% 42%))`;
30953 }
30954 _presenceForRender() {
30955 const raw = this._attr("presence");
30956 if (raw && VALID_PRESENCE.has(raw)) {
30957 return raw;
30958 }
30959 return null;
30960 }
30961 _presenceLabel(p) {
30962 switch (p) {
30963 case "online":
30964 return "Online";
30965 case "inactive":
30966 return "Inactive";
30967 case "offline":
30968 return "Offline";
30969 }
30970 }
30971 _onImgError() {
30972 this._imgFailed = true;
30973 this.requestUpdate();
30974 }
30975 _onClick(e) {
30976 const userId = this._attr("user-id");
30977 const detail = {
30978 userId: userId !== null ? Number(userId) || null : null,
30979 originalEvent: e
30980 };
30981 this.emit("wpd-avatar-click", detail);
30982 }
30983 /**
30984 * Wire up the pointer-driven tilt + glare. Listens on the host so
30985 * one set of bindings covers both the clickable `<button>` and
30986 * the decorative `<div>` rendering branches. The actual math
30987 * runs in `_handlePointerMove`; this method just owns the
30988 * bind/unbind plumbing.
30989 *
30990 * Bails entirely when `prefers-reduced-motion: reduce` is set —
30991 * the CSS has its own `@media` guard for the visual layer, but
30992 * skipping the JS too saves the per-event work for users who
30993 * won't benefit from it.
30994 */
30995 _attachHoverEffect() {
30996 const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
30997 if (reduceMotion) {
30998 return;
30999 }
31000 this._onPointerEnter = () => {
31001 this.style.setProperty("--wpd-avatar-hover", "1");
31002 };
31003 this._onPointerLeave = () => {
31004 this.style.setProperty("--wpd-avatar-hover", "0");
31005 this._pendingTiltX = "0deg";
31006 this._pendingTiltY = "0deg";
31007 this._pendingGlareX = "50%";
31008 this._pendingGlareY = "50%";
31009 this._flushTilt();
31010 };
31011 this._onPointerMove = (e) => {
31012 const rect = this.getBoundingClientRect();
31013 if (rect.width === 0 || rect.height === 0) {
31014 return;
31015 }
31016 const nx = (e.clientX - rect.left) / rect.width - 0.5;
31017 const ny = (e.clientY - rect.top) / rect.height - 0.5;
31018 const MAX = 14;
31019 this._pendingTiltY = `${(nx * MAX).toFixed(2)}deg`;
31020 this._pendingTiltX = `${(-ny * MAX).toFixed(2)}deg`;
31021 const gx = Math.max(0, Math.min(100, (nx + 0.5) * 100));
31022 const gy = Math.max(0, Math.min(100, (ny + 0.5) * 100));
31023 this._pendingGlareX = `${gx.toFixed(1)}%`;
31024 this._pendingGlareY = `${gy.toFixed(1)}%`;
31025 if (!this._tiltRaf) {
31026 this._tiltRaf = requestAnimationFrame(() => this._flushTilt());
31027 }
31028 };
31029 this.addEventListener("pointerenter", this._onPointerEnter);
31030 this.addEventListener("pointerleave", this._onPointerLeave);
31031 this.addEventListener("pointermove", this._onPointerMove);
31032 }
31033 _flushTilt() {
31034 this._tiltRaf = 0;
31035 this.style.setProperty("--wpd-avatar-tilt-x", this._pendingTiltX);
31036 this.style.setProperty("--wpd-avatar-tilt-y", this._pendingTiltY);
31037 this.style.setProperty("--wpd-avatar-glare-x", this._pendingGlareX);
31038 this.style.setProperty("--wpd-avatar-glare-y", this._pendingGlareY);
31039 }
31040 _detachHoverEffect() {
31041 if (this._onPointerMove) {
31042 this.removeEventListener("pointermove", this._onPointerMove);
31043 this._onPointerMove = null;
31044 }
31045 if (this._onPointerEnter) {
31046 this.removeEventListener("pointerenter", this._onPointerEnter);
31047 this._onPointerEnter = null;
31048 }
31049 if (this._onPointerLeave) {
31050 this.removeEventListener("pointerleave", this._onPointerLeave);
31051 this._onPointerLeave = null;
31052 }
31053 if (this._tiltRaf) {
31054 cancelAnimationFrame(this._tiltRaf);
31055 this._tiltRaf = 0;
31056 }
31057 }
31058 _maybeAttachPresenceListener() {
31059 const userId = this._attr("user-id");
31060 const explicit = this._attr("presence");
31061 const wantsListener = !!userId && !explicit;
31062 if (wantsListener && !this._presenceHandler) {
31063 this._presenceHandler = (e) => {
31064 const detail = e.detail;
31065 if (!detail) {
31066 return;
31067 }
31068 if (String(detail.userId) !== String(userId)) {
31069 return;
31070 }
31071 if (detail.newStatus && VALID_PRESENCE.has(detail.newStatus)) {
31072 this.setAttribute("presence", detail.newStatus);
31073 }
31074 };
31075 document.addEventListener(
31076 "desktop-mode-presence-changed",
31077 this._presenceHandler
31078 );
31079 } else if (!wantsListener && this._presenceHandler) {
31080 document.removeEventListener(
31081 "desktop-mode-presence-changed",
31082 this._presenceHandler
31083 );
31084 this._presenceHandler = null;
31085 }
31086 }
31087 };
31088 _WpdAvatar.props = ["src", "alt", "name", "size", "presence", "userId", "clickable"];
31089 _WpdAvatar.styles = [avatarStyles];
31090 _WpdAvatar.help = {
31091 title: "Avatar",
31092 summary: "Image-or-initials user tile with an optional presence dot. Falls back to a deterministic-hue letter tile when src is empty. Set user-id to auto-subscribe the dot to desktop-mode-presence-changed.",
31093 status: "stable",
31094 since: "0.6.0",
31095 props: [
31096 { name: "src", type: "string", description: "Image URL. Falls back to initials when empty or load fails." },
31097 { name: "alt", type: "string", description: "Alt text for the image. Defaults to `name` when omitted." },
31098 { name: "name", type: "string", description: "Used for initials + hue fallback when no src." },
31099 {
31100 name: "size",
31101 type: 'number | "xs" | "sm" | "md" | "lg" | "xl"',
31102 description: "Pixel size or named preset. Default 32 (sm-ish). Sets --wpd-avatar-size."
31103 },
31104 {
31105 name: "presence",
31106 type: '"online" | "inactive" | "offline"',
31107 description: "Presence dot color. Omit for no dot."
31108 },
31109 {
31110 name: "user-id",
31111 type: "number",
31112 description: "When set AND presence is unset, auto-subscribes to desktop-mode-presence-changed and updates the dot."
31113 },
31114 {
31115 name: "clickable",
31116 type: "boolean attribute",
31117 description: "Renders the tile as a focusable button that emits wpd-avatar-click. Omit for a decorative tile that lets clicks pass through to the surrounding row."
31118 }
31119 ],
31120 events: [
31121 {
31122 name: "wpd-avatar-click",
31123 description: "Fires on click when the `clickable` attribute is set. Detail carries userId when set.",
31124 detail: "{ userId: number | null }"
31125 }
31126 ],
31127 cssProps: [
31128 { name: "--wpd-avatar-size", description: "Tile size in any CSS length. Set automatically by the size attribute." },
31129 { name: "--wpd-avatar-dot-ring", description: "Background color used as the dot ring (matches surrounding panel by default)." }
31130 ],
31131 example: html`
31132 <wpd-avatar name="Daniel" size="40" presence="online"></wpd-avatar>
31133 `
31134 };
31135 let WpdAvatar = _WpdAvatar;
31136 defineComponent("wpd-avatar", WpdAvatar);
31137 const NOTE_COLORS = [
31138 "butter",
31139 "blush",
31140 "sky",
31141 "mint",
31142 "lilac",
31143 "peach"
31144 ];
31145 function normalizeNoteColor(color) {
31146 return NOTE_COLORS.includes(color) ? color : NOTE_COLORS[0];
31147 }
31148 function sanitizeNoteColorSlug(color) {
31149 const slug = color.toLowerCase().replace(/[^a-z0-9_-]/g, "");
31150 return slug || NOTE_COLORS[0];
31151 }
31152 function nextNoteColor(color) {
31153 const index2 = NOTE_COLORS.indexOf(
31154 normalizeNoteColor(color)
31155 );
31156 return NOTE_COLORS[(index2 + 1) % NOTE_COLORS.length];
31157 }
31158 const EASE_GLIDE = "cubic-bezier(0.2, 0.7, 0.2, 1)";
31159 const EASE_FALL = "cubic-bezier(0.5, 0, 0.9, 0.4)";
31160 const EASE_OVERSHOOT = "cubic-bezier(0.2, 0.7, 0.3, 1.15)";
31161 function prefersReducedMotion() {
31162 return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
31163 }
31164 function animate(el, keyframes, options) {
31165 if (typeof el.animate !== "function") {
31166 return Promise.resolve();
31167 }
31168 return el.animate(keyframes, options).finished.then(() => void 0).catch(() => void 0);
31169 }
31170 async function playPinInsertion(parts) {
31171 const rot = parts.restRotation ?? 0;
31172 if (prefersReducedMotion()) {
31173 spawnRipple(parts.rippleHost, 400);
31174 return;
31175 }
31176 const tempo = parts.tempo ?? 1;
31177 const fall = parts.fallDistance ?? 30;
31178 await animate(
31179 parts.pin,
31180 [
31181 {
31182 opacity: 0,
31183 transform: `translate(8px, ${-fall}px) rotate(${rot - 16}deg) scale(1.9)`
31184 },
31185 { opacity: 1, offset: 0.35 },
31186 {
31187 opacity: 1,
31188 transform: `translate(0, 0) rotate(${rot}deg) scale(0.92)`
31189 }
31190 ],
31191 { duration: 170 * tempo, easing: EASE_FALL, fill: "forwards" }
31192 );
31193 spawnRipple(parts.rippleHost, 420 * tempo);
31194 const paperSettle = animate(
31195 parts.paper,
31196 [
31197 { transform: "scale(1) translateY(0) skewX(0)" },
31198 {
31199 transform: "scale(0.982) translateY(2px) skewX(0)",
31200 offset: 0.08
31201 },
31202 {
31203 transform: "scale(1.01) translateY(0) skewX(0.6deg)",
31204 offset: 0.32
31205 },
31206 {
31207 transform: "scale(0.996) translateY(0) skewX(-0.4deg)",
31208 offset: 0.55
31209 },
31210 { transform: "scale(1) translateY(0) skewX(0)" }
31211 ],
31212 { duration: 370 * tempo, easing: "linear" }
31213 );
31214 const pinSettle = animate(
31215 parts.pin,
31216 [
31217 { transform: `rotate(${rot}deg) scale(0.92)` },
31218 { transform: `rotate(${rot + 1.5}deg) scale(1.07)`, offset: 0.25 },
31219 { transform: `rotate(${rot - 1}deg) scale(0.98)`, offset: 0.5 },
31220 { transform: `rotate(${rot}deg) scale(1)` }
31221 ],
31222 { duration: 370 * tempo, easing: EASE_GLIDE, fill: "forwards" }
31223 );
31224 await Promise.all([paperSettle, pinSettle]);
31225 }
31226 function spawnRipple(host, duration) {
31227 const ripple = document.createElement("span");
31228 ripple.className = "desktop-mode-pinned-note__ripple";
31229 ripple.setAttribute("aria-hidden", "true");
31230 host.appendChild(ripple);
31231 void animate(
31232 ripple,
31233 [
31234 { boxShadow: "0 0 0 0 rgba(0, 0, 0, 0.28)", opacity: 0.35 },
31235 { boxShadow: "0 0 0 26px rgba(0, 0, 0, 0)", opacity: 0 }
31236 ],
31237 { duration, easing: "ease-out" }
31238 ).then(() => ripple.remove());
31239 window.setTimeout(() => ripple.remove(), duration + 100);
31240 }
31241 function startPendulum(swingEl) {
31242 if (prefersReducedMotion()) {
31243 return {
31244 onPointerMove: () => void 0,
31245 setBias: () => void 0,
31246 stop: () => void 0
31247 };
31248 }
31249 const C = 14;
31250 let angle = 0;
31251 let velocity = 0;
31252 let target2 = 0;
31253 let bias = 0;
31254 let lastX = null;
31255 let lastMoveTime = 0;
31256 let emaVx = 0;
31257 let raf = 0;
31258 let lastFrame = 0;
31259 let running = true;
31260 const frame = (now) => {
31261 if (!running) {
31262 return;
31263 }
31264 const dt = Math.min(0.05, lastFrame ? (now - lastFrame) / 1e3 : 0.016);
31265 lastFrame = now;
31266 if (now - lastMoveTime > 80) {
31267 emaVx *= 0.85;
31268 }
31269 target2 = Math.max(-14, Math.min(14, -emaVx * 0.055)) + bias;
31270 velocity += (-120 * (angle - target2) - C * velocity) * dt;
31271 angle += velocity * dt;
31272 swingEl.style.transform = `rotate(${angle.toFixed(2)}deg)`;
31273 raf = window.requestAnimationFrame(frame);
31274 };
31275 raf = window.requestAnimationFrame(frame);
31276 return {
31277 onPointerMove(clientX) {
31278 const now = performance.now();
31279 if (lastX !== null && now > lastMoveTime) {
31280 const instVx = (clientX - lastX) / (now - lastMoveTime) * 16.7;
31281 emaVx = emaVx * 0.7 + instVx * 0.3;
31282 }
31283 lastX = clientX;
31284 lastMoveTime = now;
31285 },
31286 setBias(deg) {
31287 bias = deg;
31288 },
31289 stop() {
31290 running = false;
31291 window.cancelAnimationFrame(raf);
31292 swingEl.style.transform = "";
31293 }
31294 };
31295 }
31296 async function playSnapBack(parts) {
31297 if (prefersReducedMotion()) {
31298 await animate(parts.flyback, [{ opacity: 1 }, { opacity: 0 }], {
31299 duration: 120,
31300 easing: "linear",
31301 fill: "forwards"
31302 });
31303 return;
31304 }
31305 const yank = parts.swing ? animate(
31306 parts.swing,
31307 [
31308 { transform: "rotate(0deg)" },
31309 { transform: "rotate(9deg)", offset: 0.35 },
31310 { transform: "rotate(-4deg)", offset: 0.7 },
31311 { transform: "rotate(0deg)" }
31312 ],
31313 { duration: 330, easing: "linear", fill: "forwards" }
31314 ) : Promise.resolve();
31315 const fly = animate(
31316 parts.flyback,
31317 [
31318 {
31319 left: `${parts.flyback.offsetLeft}px`,
31320 top: `${parts.flyback.offsetTop}px`
31321 },
31322 { left: `${parts.homeX}px`, top: `${parts.homeY}px` }
31323 ],
31324 { duration: 330, easing: EASE_OVERSHOOT, fill: "forwards" }
31325 );
31326 await Promise.all([fly, yank]);
31327 }
31328 async function playCrumpleIntoBin(parts) {
31329 if (prefersReducedMotion()) {
31330 await animate(parts.clone, [{ opacity: 1 }, { opacity: 0 }], {
31331 duration: 150,
31332 easing: "linear",
31333 fill: "forwards"
31334 });
31335 return;
31336 }
31337 if (parts.pin) {
31338 void animate(
31339 parts.pin,
31340 [
31341 { transform: "translate(0, 0) rotate(0deg)", opacity: 1 },
31342 { transform: "translate(2px, -16px) rotate(-30deg)", opacity: 0 }
31343 ],
31344 { duration: 200, easing: EASE_GLIDE, fill: "forwards" }
31345 );
31346 }
31347 const rect = parts.clone.getBoundingClientRect();
31348 const dx = parts.binX - (rect.left + rect.width / 2);
31349 const dy = parts.binY - (rect.top + rect.height / 2);
31350 const rough1 = "polygon(4% 8%, 38% 2%, 68% 7%, 96% 3%, 98% 42%, 92% 71%, 97% 94%, 60% 98%, 30% 93%, 3% 97%, 6% 62%, 2% 34%)";
31351 const rough2 = "polygon(10% 14%, 42% 6%, 64% 12%, 90% 8%, 94% 38%, 86% 66%, 92% 88%, 58% 94%, 34% 86%, 10% 92%, 14% 58%, 8% 36%)";
31352 await animate(
31353 parts.paper,
31354 [
31355 {
31356 transform: "translate(0, 0) scale(0.88) rotate(0deg)",
31357 borderRadius: "2px",
31358 opacity: 1
31359 },
31360 {
31361 transform: `translate(${dx * 0.3}px, ${dy * 0.3}px) scale(0.6) rotate(12deg)`,
31362 clipPath: rough1,
31363 offset: 0.3
31364 },
31365 {
31366 transform: `translate(${dx * 0.6}px, ${dy * 0.6}px) scale(0.34) rotate(24deg)`,
31367 clipPath: rough2,
31368 offset: 0.6,
31369 opacity: 1
31370 },
31371 {
31372 transform: `translate(${dx}px, ${dy}px) scale(0.12) rotate(38deg)`,
31373 borderRadius: "50%",
31374 clipPath: rough2,
31375 opacity: 0
31376 }
31377 ],
31378 { duration: 400, easing: EASE_FALL, delay: 60, fill: "forwards" }
31379 );
31380 }
31381 function hashNoteSeed(text) {
31382 let hash2 = 2166136261;
31383 for (let i = 0; i < text.length; i++) {
31384 hash2 ^= text.charCodeAt(i);
31385 hash2 = Math.imul(hash2, 16777619) >>> 0;
31386 }
31387 const seed2 = hash2 >>> 1 || 1;
31388 return seed2;
31389 }
31390 function noteJitter(seed2) {
31391 let hash2 = 2166136261;
31392 const key = `wpd-note-${seed2}`;
31393 for (let i = 0; i < key.length; i++) {
31394 hash2 ^= key.charCodeAt(i);
31395 hash2 = Math.imul(hash2, 16777619) >>> 0;
31396 }
31397 const shifted3 = hash2 >>> 3;
31398 const shifted5 = hash2 >>> 5;
31399 return {
31400 rotation: (hash2 % 45 - 22) / 10,
31401 // ±2.2°
31402 pinOffsetX: shifted3 % 21 - 10,
31403 // ±10 px
31404 pinRotation: shifted5 % 17 - 8
31405 // ±8°
31406 };
31407 }
31408 const PIN_TIP_X = 0.57;
31409 const PIN_TIP_Y = 0.525;
31410 const PIN_WIDTH = 56;
31411 const PIN_HEIGHT = 52;
31412 function pushpinUrl(pluginUrl) {
31413 return `${pluginUrl.replace(/\/$/, "")}/assets/images/pushpin.svg`;
31414 }
31415 function buildPinImage(pluginUrl) {
31416 const img = document.createElement("img");
31417 img.src = pushpinUrl(pluginUrl);
31418 img.alt = "";
31419 img.width = PIN_WIDTH;
31420 img.height = PIN_HEIGHT;
31421 img.draggable = false;
31422 img.className = "desktop-mode-pinned-note__pin-img";
31423 return img;
31424 }
31425 let deps = null;
31426 function installNotesRestDeps(next) {
31427 deps = next;
31428 }
31429 function ensureDeps() {
31430 if (!deps) {
31431 throw new Error(
31432 "[desktop-mode] notes REST client called before installNotesRestDeps()."
31433 );
31434 }
31435 return deps;
31436 }
31437 function liveNonce(installed2) {
31438 const cfg = window.desktopModeConfig;
31439 return typeof cfg?.restNonce === "string" && cfg.restNonce ? cfg.restNonce : installed2;
31440 }
31441 class NotesConflictError extends Error {
31442 constructor(current) {
31443 super("Note was changed by another session.");
31444 this.status = 409;
31445 this.name = "NotesConflictError";
31446 this.current = current;
31447 }
31448 }
31449 function isNotesConflict(err) {
31450 return err instanceof NotesConflictError;
31451 }
31452 async function call(path, init2) {
31453 const { baseUrl, nonce } = ensureDeps();
31454 const url = path ? joinRestUrl(baseUrl, path) : baseUrl;
31455 const headers = new Headers(init2.headers ?? {});
31456 headers.set("X-WP-Nonce", liveNonce(nonce));
31457 if (init2.body && !headers.has("Content-Type")) {
31458 headers.set("Content-Type", "application/json");
31459 }
31460 const res = await trackedFetch$1(
31461 url,
31462 { ...init2, headers, credentials: "same-origin" },
31463 { source: "desktop-mode/notes" }
31464 );
31465 const text = await res.text();
31466 let body = null;
31467 if (text) {
31468 try {
31469 body = JSON.parse(text);
31470 } catch {
31471 body = null;
31472 }
31473 }
31474 if (!res.ok) {
31475 if (res.status === 409) {
31476 const current = body?.data?.current;
31477 throw new NotesConflictError(current ?? null);
31478 }
31479 const err = body;
31480 throw new Error(
31481 `[desktop-mode] notes REST ${res.status}: ${err?.code ?? ""} ${err?.message ?? ""}`.trim()
31482 );
31483 }
31484 if (null === body) {
31485 throw new Error(
31486 `[desktop-mode] notes REST ${res.status}: empty or unparseable body.`
31487 );
31488 }
31489 return body;
31490 }
31491 function listNotes() {
31492 return call("", { method: "GET" });
31493 }
31494 function createNote(body) {
31495 return call("", {
31496 method: "POST",
31497 body: JSON.stringify(body)
31498 });
31499 }
31500 function updateNote(id, body) {
31501 return call(`/${id}`, {
31502 method: "PATCH",
31503 body: JSON.stringify(body)
31504 });
31505 }
31506 function deleteNote(id) {
31507 return call(`/${id}`, {
31508 method: "DELETE"
31509 });
31510 }
31511 function restoreNote(id) {
31512 return call(`/${id}/restore`, { method: "POST" });
31513 }
31514 function convertNote(id) {
31515 return call(`/${id}/convert`, { method: "POST" });
31516 }
31517 const SUBSCRIBE_FIELD = "desktop_mode_notes_subscribe";
31518 const RESPONSE_FIELD = "desktop_mode_notes";
31519 let started$3 = false;
31520 let target = null;
31521 function startNotesHeartbeat(nextTarget) {
31522 target = nextTarget;
31523 if (started$3) {
31524 return;
31525 }
31526 started$3 = true;
31527 heartbeat.contribute(
31528 SUBSCRIBE_FIELD,
31529 () => target?.getHeartbeatSubscription()
31530 );
31531 heartbeat.subscribe(RESPONSE_FIELD, (payload) => {
31532 target?.applyHeartbeatPayload(payload);
31533 });
31534 }
31535 function getToastApi() {
31536 const api = window.wp?.desktop;
31537 return api && typeof api.showToast === "function" ? api : null;
31538 }
31539 async function trashNoteWithUndo(note, callbacks) {
31540 callbacks.onEvict(note.id);
31541 try {
31542 await deleteNote(note.id);
31543 getToastApi()?.showToast?.({
31544 message: __("Note moved to Trash", "desktop-mode"),
31545 duration: 6e3,
31546 action: {
31547 label: __("Undo", "desktop-mode"),
31548 onClick: () => {
31549 void restoreNote(note.id).then((restored) => callbacks.onRestore(restored)).catch((err) => {
31550 console.error(
31551 "[desktop-mode] notes: restore failed:",
31552 err
31553 );
31554 });
31555 }
31556 }
31557 });
31558 } catch (err) {
31559 console.error("[desktop-mode] notes: trash failed:", err);
31560 callbacks.onRestore(note);
31561 getToastApi()?.showToast?.({
31562 message: __("Could not move the note to the Trash.", "desktop-mode"),
31563 duration: 5e3
31564 });
31565 }
31566 }
31567 function getDesktopApi() {
31568 return window.wp?.desktop ?? null;
31569 }
31570 function openDraftEditor(url) {
31571 const api = getDesktopApi();
31572 if (!api?.windowManager?.open || !api.deriveWindowId) {
31573 window.location.href = url;
31574 return null;
31575 }
31576 const id = api.deriveWindowId(url);
31577 api.windowManager.open({
31578 id,
31579 baseId: id,
31580 url,
31581 title: __("Edit draft", "desktop-mode"),
31582 icon: "dashicons-admin-post"
31583 });
31584 return id;
31585 }
31586 async function convertNoteToPost(note, callbacks) {
31587 callbacks.onEvict(note.id);
31588 try {
31589 const result = await convertNote(note.id);
31590 const editorWindowId = openDraftEditor(result.editUrl);
31591 getDesktopApi()?.showToast?.({
31592 message: __("Note converted to a draft post", "desktop-mode"),
31593 duration: 6e3,
31594 action: {
31595 label: __("Undo", "desktop-mode"),
31596 onClick: () => {
31597 if (editorWindowId) {
31598 getDesktopApi()?.windowManager?.getById?.(editorWindowId)?.close?.();
31599 }
31600 void restoreNote(note.id).then((restored) => callbacks.onRestore(restored)).catch((err) => {
31601 console.error(
31602 "[desktop-mode] notes: convert undo failed:",
31603 err
31604 );
31605 });
31606 }
31607 }
31608 });
31609 } catch (err) {
31610 console.error("[desktop-mode] notes: convert failed:", err);
31611 callbacks.onRestore(note);
31612 getDesktopApi()?.showToast?.({
31613 message: __("Could not convert the note to a post.", "desktop-mode"),
31614 duration: 5e3
31615 });
31616 }
31617 }
31618 const NOTE_DRAFT_PAYLOAD_TYPE = "note-draft";
31619 const NOTE_PAYLOAD_TYPE = "note";
31620 const NOTE_CREATED_EVENT = "desktop-mode-note-created";
31621 const NOTE_WIDTH = 208;
31622 const SAVE_DEBOUNCE_MS = 1e3;
31623 const Z_SAVE_DEBOUNCE_MS = 800;
31624 const KEYBOARD_STEP_PX = 10;
31625 const KEYBOARD_FINE_STEP_PX = 1;
31626 function getDragManager$1() {
31627 const api = window.wp?.desktop?.dragManager;
31628 return api ?? null;
31629 }
31630 function jitterSeed(note) {
31631 return note.seed || Math.abs(note.id) || 1;
31632 }
31633 const ICON_POST = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true" focusable="false"><path d="m7.3 9.7 1.4 1.4c.2-.2.3-.3.4-.5 0 0 0-.1.1-.1.3-.5.4-1.1.3-1.6L12 7 9 4 7.2 6.5c-.6-.1-1.1 0-1.6.3 0 0-.1 0-.1.1-.3.1-.4.2-.6.4l1.4 1.4L4 11v1h1l2.3-2.3zM4 20h9v-1.5H4V20zm0-5.5V16h16v-1.5H4z" /></svg>`;
31634 class NotesLayer {
31635 constructor(options) {
31636 this.root = null;
31637 this.liveRegion = null;
31638 this.controllers = /* @__PURE__ */ new Map();
31639 this.zCounter = 1;
31640 this.highWaterMs = 0;
31641 this.tempIdCounter = 0;
31642 this.host = options.host;
31643 this.pluginUrl = options.pluginUrl;
31644 this.canCreatePosts = options.canCreatePosts ?? false;
31645 this.onError = options.onError;
31646 }
31647 async boot() {
31648 try {
31649 const { notes } = await listNotes();
31650 notes.forEach((note) => {
31651 this.bumpHighWater(note.updatedAtMs);
31652 this.upsertNote(note, { animate: "none" });
31653 });
31654 startNotesHeartbeat(this);
31655 } catch (error) {
31656 if (error instanceof Error) {
31657 console.debug("[desktop-mode] Pinned notes unavailable:", error.message);
31658 }
31659 }
31660 }
31661 /**
31662 * Insert or update a note on the wall.
31663 */
31664 upsertNote(note, options = { animate: "none" }) {
31665 this.ensureRoot();
31666 this.zCounter = Math.max(this.zCounter, note.z);
31667 const existing = this.controllers.get(note.id);
31668 if (existing) {
31669 existing.replace(note);
31670 return existing;
31671 }
31672 const controller = new NoteController({ layer: this, note });
31673 this.controllers.set(note.id, controller);
31674 this.root?.appendChild(controller.element);
31675 if (options.animate !== "none") {
31676 void controller.playInsertion(options.animate === "thunk" ? 1 : 0.7);
31677 }
31678 return controller;
31679 }
31680 removeNote(noteId) {
31681 const controller = this.controllers.get(noteId);
31682 if (!controller) {
31683 return;
31684 }
31685 this.controllers.delete(noteId);
31686 controller.dispose();
31687 controller.element.remove();
31688 }
31689 /** Rebind a controller after a temp (optimistic) id resolves. */
31690 rekeyNote(oldId, controller) {
31691 this.controllers.delete(oldId);
31692 this.controllers.set(controller.note.id, controller);
31693 }
31694 has(noteId) {
31695 return this.controllers.has(noteId);
31696 }
31697 get(noteId) {
31698 return this.controllers.get(noteId);
31699 }
31700 nextTempId() {
31701 this.tempIdCounter -= 1;
31702 return this.tempIdCounter;
31703 }
31704 bringToFront(controller) {
31705 this.zCounter += 1;
31706 controller.setZ(this.zCounter);
31707 }
31708 bumpHighWater(updatedAtMs) {
31709 if (Number.isFinite(updatedAtMs) && updatedAtMs > this.highWaterMs) {
31710 this.highWaterMs = updatedAtMs;
31711 }
31712 }
31713 hostSize() {
31714 return {
31715 width: Math.max(1, this.host.clientWidth),
31716 height: Math.max(1, this.host.clientHeight)
31717 };
31718 }
31719 /** Clamp a normalized position so the note stays reachable. */
31720 clampPosition(x, y) {
31721 const { width, height } = this.hostSize();
31722 const maxX = Math.max(0, 1 - NOTE_WIDTH / width);
31723 const maxY = Math.max(0, 1 - 120 / height);
31724 return {
31725 x: Math.min(maxX, Math.max(0, x)),
31726 y: Math.min(maxY, Math.max(0, y))
31727 };
31728 }
31729 announce(message) {
31730 this.ensureRoot();
31731 if (this.liveRegion) {
31732 this.liveRegion.textContent = message;
31733 }
31734 }
31735 notifyError(message) {
31736 this.onError?.(message);
31737 }
31738 trashNote(note) {
31739 void trashNoteWithUndo(note, {
31740 onEvict: (noteId) => this.removeNote(noteId),
31741 onRestore: (restored) => {
31742 this.bumpHighWater(restored.updatedAtMs);
31743 this.upsertNote(restored, { animate: "move" });
31744 }
31745 });
31746 }
31747 /**
31748 * Convert a note to a draft post: evict optimistically, auto-open
31749 * the draft editor, Undo restores the note (and discards the draft).
31750 */
31751 convertNote(note) {
31752 if (!this.canCreatePosts || !note.canEdit) {
31753 return;
31754 }
31755 void convertNoteToPost(note, {
31756 onEvict: (noteId) => this.removeNote(noteId),
31757 onRestore: (restored) => {
31758 this.bumpHighWater(restored.updatedAtMs);
31759 this.upsertNote(restored, { animate: "move" });
31760 }
31761 });
31762 }
31763 // ------------------------------------------------------------------
31764 // Heartbeat
31765 // ------------------------------------------------------------------
31766 getHeartbeatSubscription() {
31767 return {
31768 knownIds: Array.from(this.controllers.keys()).filter((id) => id > 0),
31769 sinceMs: this.highWaterMs
31770 };
31771 }
31772 applyHeartbeatPayload(payload) {
31773 for (const note of payload.notes ?? []) {
31774 this.bumpHighWater(note.updatedAtMs);
31775 const existing = this.controllers.get(note.id);
31776 if (existing) {
31777 if (existing.shouldReplaceFromRemote(note)) {
31778 existing.replace(note);
31779 }
31780 } else {
31781 this.upsertNote(note, { animate: "move" });
31782 }
31783 }
31784 for (const id of payload.removed ?? []) {
31785 this.removeNote(id);
31786 }
31787 if (typeof payload.serverTimeMs === "number" && Number.isFinite(payload.serverTimeMs)) {
31788 this.bumpHighWater(payload.serverTimeMs);
31789 }
31790 if (payload.truncated) {
31791 void this.reloadFromServer();
31792 }
31793 }
31794 /** Full re-hydration — the Heartbeat delta overflowed its cap. */
31795 async reloadFromServer() {
31796 try {
31797 const { notes } = await listNotes();
31798 const alive = /* @__PURE__ */ new Set();
31799 for (const note of notes) {
31800 alive.add(note.id);
31801 this.bumpHighWater(note.updatedAtMs);
31802 const existing = this.controllers.get(note.id);
31803 if (existing) {
31804 if (existing.shouldReplaceFromRemote(note)) {
31805 existing.replace(note);
31806 }
31807 } else {
31808 this.upsertNote(note, { animate: "move" });
31809 }
31810 }
31811 for (const id of Array.from(this.controllers.keys())) {
31812 if (id > 0 && !alive.has(id)) {
31813 this.removeNote(id);
31814 }
31815 }
31816 } catch {
31817 }
31818 }
31819 ensureRoot() {
31820 if (this.root) {
31821 return this.root;
31822 }
31823 const root = document.createElement("section");
31824 root.className = "desktop-mode-notes";
31825 root.setAttribute("aria-label", __("Pinned notes", "desktop-mode"));
31826 const live = document.createElement("div");
31827 live.className = "desktop-mode-notes__live screen-reader-text";
31828 live.setAttribute("aria-live", "polite");
31829 root.appendChild(live);
31830 this.host.appendChild(root);
31831 this.root = root;
31832 this.liveRegion = live;
31833 return root;
31834 }
31835 }
31836 class NoteController {
31837 constructor(options) {
31838 this.editor = null;
31839 this.statusEl = null;
31840 this.colorDot = null;
31841 this.visibilityBtn = null;
31842 this.saveTimer = null;
31843 this.zTimer = null;
31844 this.patchChain = Promise.resolve();
31845 this.pendingText = null;
31846 this.disposed = false;
31847 this.moveMode = false;
31848 this.moveOrigin = null;
31849 this.dragCleanup = null;
31850 this.lastPointer = null;
31851 this.layer = options.layer;
31852 this.note = options.note;
31853 this.jitter = noteJitter(jitterSeed(options.note));
31854 this.element = document.createElement("article");
31855 this.paperEl = document.createElement("div");
31856 this.pinEl = document.createElement(
31857 this.note.canEdit ? "button" : "span"
31858 );
31859 this.paint();
31860 this.applyPosition();
31861 this.setZ(this.note.z);
31862 this.element.addEventListener(
31863 "pointerdown",
31864 () => this.layer.bringToFront(this),
31865 { capture: true }
31866 );
31867 }
31868 // ------------------------------------------------------------------
31869 // DOM
31870 // ------------------------------------------------------------------
31871 paint() {
31872 const note = this.note;
31873 this.element.className = "desktop-mode-pinned-note";
31874 this.element.dataset.noteId = String(note.id);
31875 this.element.dataset.noteColor = sanitizeNoteColorSlug(note.color);
31876 this.element.dataset.owner = note.canEdit ? "me" : "other";
31877 this.element.setAttribute(
31878 "role",
31879 note.canEdit ? "group" : "note"
31880 );
31881 this.element.setAttribute(
31882 "aria-label",
31883 note.canEdit ? __("Pinned note", "desktop-mode") : sprintf(
31884 /* translators: %s: note author display name. */
31885 __("Note by %s", "desktop-mode"),
31886 note.ownerName
31887 )
31888 );
31889 this.element.style.setProperty("--dm-note-rot", `${this.jitter.rotation}deg`);
31890 this.element.style.setProperty("--dm-pin-dx", `${this.jitter.pinOffsetX}px`);
31891 this.element.style.setProperty("--dm-pin-rot", `${this.jitter.pinRotation}deg`);
31892 this.pinEl.className = "desktop-mode-pinned-note__pin";
31893 this.pinEl.appendChild(buildPinImage(this.layer.pluginUrl));
31894 if (note.canEdit) {
31895 this.pinEl.setAttribute("type", "button");
31896 this.pinEl.setAttribute("aria-pressed", "false");
31897 this.pinEl.setAttribute(
31898 "aria-label",
31899 __(
31900 "Move note. Drag the pin, or press Enter then use the arrow keys.",
31901 "desktop-mode"
31902 )
31903 );
31904 this.pinEl.addEventListener(
31905 "pointerdown",
31906 (event) => this.startDrag(event)
31907 );
31908 this.pinEl.addEventListener("click", (event) => {
31909 if (event.detail === 0 && !this.moveMode) {
31910 this.toggleMoveMode();
31911 }
31912 });
31913 this.pinEl.addEventListener(
31914 "keydown",
31915 (event) => this.onPinKeydown(event)
31916 );
31917 this.pinEl.addEventListener("blur", () => this.exitMoveMode(false));
31918 } else {
31919 this.pinEl.setAttribute("aria-hidden", "true");
31920 }
31921 this.paperEl.className = "desktop-mode-pinned-note__paper";
31922 if (note.canEdit) {
31923 this.paintOwnerPaper();
31924 } else {
31925 this.paintViewerPaper();
31926 }
31927 this.element.append(this.pinEl, this.paperEl);
31928 }
31929 paintOwnerPaper() {
31930 const meta = document.createElement("div");
31931 meta.className = "desktop-mode-pinned-note__meta";
31932 const colorDot = document.createElement("button");
31933 colorDot.type = "button";
31934 colorDot.className = "desktop-mode-pinned-note__color-dot";
31935 this.colorDot = colorDot;
31936 this.refreshColorDot();
31937 colorDot.addEventListener("click", () => this.cycleColor());
31938 const visibility = document.createElement("wpd-window-button");
31939 visibility.className = "desktop-mode-pinned-note__visibility";
31940 this.visibilityBtn = visibility;
31941 this.refreshVisibility();
31942 visibility.addEventListener(
31943 "wpd-button-activate",
31944 () => this.togglePublic()
31945 );
31946 meta.append(colorDot, visibility);
31947 if (this.layer.canCreatePosts) {
31948 const convert = document.createElement("wpd-window-button");
31949 convert.className = "desktop-mode-pinned-note__convert";
31950 convert.innerHTML = ICON_POST;
31951 const convertLabel = __("Convert to a draft post", "desktop-mode");
31952 convert.setAttribute("title", convertLabel);
31953 convert.setAttribute("aria-label", convertLabel);
31954 convert.addEventListener(
31955 "wpd-button-activate",
31956 () => this.layer.convertNote(this.note)
31957 );
31958 meta.append(convert);
31959 }
31960 const editor = document.createElement("wpd-textarea");
31961 editor.className = "desktop-mode-pinned-note__editor";
31962 editor.setAttribute("aria-label", __("Note text", "desktop-mode"));
31963 editor.setAttribute("rows", "5");
31964 editor.setAttribute("auto-grow", "");
31965 editor.setAttribute("max-rows", "10");
31966 editor.setAttribute("value", this.note.text);
31967 ["keydown", "keypress", "keyup"].forEach((eventName) => {
31968 editor.addEventListener(eventName, (event) => event.stopPropagation());
31969 });
31970 editor.addEventListener("wpd-input-change", (event) => {
31971 const detail = event.detail;
31972 this.pendingText = detail.value;
31973 this.setPhase("pending");
31974 this.scheduleSave();
31975 });
31976 editor.addEventListener("wpd-input-commit", () => this.flushSave());
31977 this.editor = editor;
31978 const footer = document.createElement("div");
31979 footer.className = "desktop-mode-pinned-note__footer";
31980 const status = document.createElement("wpd-save-status");
31981 status.setAttribute("mode", "icon");
31982 status.setAttribute("phase", "idle");
31983 status.className = "desktop-mode-pinned-note__status";
31984 this.statusEl = status;
31985 footer.appendChild(status);
31986 this.paperEl.append(meta, editor, footer);
31987 }
31988 paintViewerPaper() {
31989 const body = document.createElement("div");
31990 body.className = "desktop-mode-pinned-note__body";
31991 body.textContent = this.note.text;
31992 const chip = document.createElement("div");
31993 chip.className = "desktop-mode-pinned-note__attribution";
31994 chip.title = sprintf(
31995 /* translators: %s: note author display name. */
31996 __("Pinned by %s", "desktop-mode"),
31997 this.note.ownerName
31998 );
31999 const avatar = document.createElement("wpd-avatar");
32000 avatar.setAttribute("size", "20");
32001 avatar.setAttribute("name", this.note.ownerName);
32002 if (this.note.ownerAvatar) {
32003 avatar.setAttribute("src", this.note.ownerAvatar);
32004 }
32005 const name = document.createElement("span");
32006 name.className = "desktop-mode-pinned-note__attribution-name";
32007 name.textContent = this.note.ownerName;
32008 chip.append(avatar, name);
32009 this.paperEl.append(body, chip);
32010 }
32011 refreshColorDot() {
32012 if (!this.colorDot) {
32013 return;
32014 }
32015 const next = nextNoteColor(this.note.color);
32016 this.colorDot.style.setProperty(
32017 "--dm-note-next-paper",
32018 `var(--dm-note-${next})`
32019 );
32020 this.colorDot.setAttribute(
32021 "aria-label",
32022 sprintf(
32023 /* translators: %s: next paper color name. */
32024 __("Change paper color (next: %s)", "desktop-mode"),
32025 next
32026 )
32027 );
32028 }
32029 refreshVisibility() {
32030 if (!this.visibilityBtn) {
32031 return;
32032 }
32033 const isPublic = this.note.public;
32034 this.visibilityBtn.innerHTML = "";
32035 const icon = document.createElement("span");
32036 icon.className = `dashicons ${isPublic ? "dashicons-admin-site-alt3" : "dashicons-lock"}`;
32037 icon.setAttribute("aria-hidden", "true");
32038 this.visibilityBtn.appendChild(icon);
32039 this.visibilityBtn.setAttribute(
32040 "title",
32041 isPublic ? __("Public note — everyone can see it. Click to make private.", "desktop-mode") : __("Private note. Click to share with every desktop user.", "desktop-mode")
32042 );
32043 this.visibilityBtn.classList.toggle("is-public", isPublic);
32044 }
32045 // ------------------------------------------------------------------
32046 // State
32047 // ------------------------------------------------------------------
32048 replace(note) {
32049 const idChanged = note.id !== this.note.id;
32050 const seedChanged = jitterSeed(note) !== jitterSeed(this.note);
32051 const colorChanged = note.color !== this.note.color;
32052 const positionChanged = note.x !== this.note.x || note.y !== this.note.y;
32053 const zChanged = note.z !== this.note.z;
32054 this.note = note;
32055 if (zChanged) {
32056 this.element.style.zIndex = String(note.z);
32057 }
32058 if (idChanged) {
32059 this.element.dataset.noteId = String(note.id);
32060 }
32061 if (seedChanged) {
32062 this.jitter = noteJitter(jitterSeed(note));
32063 this.element.style.setProperty("--dm-note-rot", `${this.jitter.rotation}deg`);
32064 this.element.style.setProperty("--dm-pin-dx", `${this.jitter.pinOffsetX}px`);
32065 this.element.style.setProperty("--dm-pin-rot", `${this.jitter.pinRotation}deg`);
32066 }
32067 if (colorChanged) {
32068 this.element.dataset.noteColor = sanitizeNoteColorSlug(note.color);
32069 this.refreshColorDot();
32070 }
32071 if (positionChanged) {
32072 this.applyPosition();
32073 }
32074 if (this.note.canEdit) {
32075 if (this.pendingText === null) {
32076 this.editor?.setAttribute("value", note.text);
32077 }
32078 this.refreshVisibility();
32079 } else {
32080 const body = this.paperEl.querySelector(
32081 ".desktop-mode-pinned-note__body"
32082 );
32083 if (body) {
32084 body.textContent = note.text;
32085 }
32086 }
32087 }
32088 /**
32089 * Remote copies never clobber local unsaved edits; otherwise
32090 * accept anything newer than what we render.
32091 */
32092 shouldReplaceFromRemote(note) {
32093 if (this.pendingText !== null || this.saveTimer !== null) {
32094 return false;
32095 }
32096 return note.updatedAtMs >= this.note.updatedAtMs;
32097 }
32098 setZ(z) {
32099 this.element.style.zIndex = String(z);
32100 if (z === this.note.z || !this.note.canEdit) {
32101 return;
32102 }
32103 this.note = { ...this.note, z };
32104 if (this.note.id <= 0) {
32105 return;
32106 }
32107 if (this.zTimer !== null) {
32108 window.clearTimeout(this.zTimer);
32109 }
32110 this.zTimer = window.setTimeout(() => {
32111 this.zTimer = null;
32112 this.queuePatch({ z: this.note.z });
32113 }, Z_SAVE_DEBOUNCE_MS);
32114 }
32115 applyPosition() {
32116 this.element.style.left = `${(this.note.x * 100).toFixed(3)}%`;
32117 this.element.style.top = `${(this.note.y * 100).toFixed(3)}%`;
32118 }
32119 /** Optimistically move + persist. Used by drop handler and keyboard. */
32120 moveTo(x, y) {
32121 const clamped = this.layer.clampPosition(x, y);
32122 this.note = { ...this.note, x: clamped.x, y: clamped.y };
32123 this.applyPosition();
32124 if (this.note.id > 0) {
32125 this.queuePatch({ x: clamped.x, y: clamped.y });
32126 }
32127 }
32128 playInsertion(tempo = 1) {
32129 return playPinInsertion({
32130 pin: this.pinEl,
32131 paper: this.paperEl,
32132 rippleHost: this.element,
32133 restRotation: this.jitter.pinRotation,
32134 fallDistance: tempo >= 1 ? 30 : 12,
32135 tempo
32136 });
32137 }
32138 focusEditor() {
32139 window.setTimeout(() => this.editor?.focusInput?.(), 0);
32140 }
32141 dispose() {
32142 this.disposed = true;
32143 if (this.saveTimer !== null) {
32144 window.clearTimeout(this.saveTimer);
32145 this.saveTimer = null;
32146 }
32147 if (this.zTimer !== null) {
32148 window.clearTimeout(this.zTimer);
32149 this.zTimer = null;
32150 }
32151 this.dragCleanup?.();
32152 this.dragCleanup = null;
32153 }
32154 // ------------------------------------------------------------------
32155 // Autosave
32156 // ------------------------------------------------------------------
32157 setPhase(phase) {
32158 this.statusEl?.setAttribute("phase", phase);
32159 }
32160 scheduleSave() {
32161 if (this.saveTimer !== null) {
32162 window.clearTimeout(this.saveTimer);
32163 }
32164 this.saveTimer = window.setTimeout(() => {
32165 this.saveTimer = null;
32166 this.flushSave();
32167 }, SAVE_DEBOUNCE_MS);
32168 }
32169 /**
32170 * Persist edits typed while the note was still optimistic (its
32171 * create POST in flight). Called by the drop handler once the
32172 * server id lands — without it, a save debounce that fired on the
32173 * temp id would strand `pendingText` forever (text lost on reload
32174 * AND remote replacement blocked, since `shouldReplaceFromRemote`
32175 * refuses while local edits are pending).
32176 */
32177 flushPendingEdits() {
32178 this.flushSave();
32179 }
32180 flushSave() {
32181 if (this.saveTimer !== null) {
32182 window.clearTimeout(this.saveTimer);
32183 this.saveTimer = null;
32184 }
32185 if (this.pendingText === null || this.note.id <= 0) {
32186 return;
32187 }
32188 const text = this.pendingText;
32189 this.pendingText = null;
32190 this.note = { ...this.note, text };
32191 this.setPhase("saving");
32192 this.queuePatch({ text });
32193 }
32194 cycleColor() {
32195 const color = nextNoteColor(this.note.color);
32196 this.note = { ...this.note, color };
32197 this.element.dataset.noteColor = color;
32198 this.refreshColorDot();
32199 if (this.note.id > 0) {
32200 this.queuePatch({ color });
32201 }
32202 }
32203 togglePublic() {
32204 const isPublic = !this.note.public;
32205 this.note = { ...this.note, public: isPublic };
32206 this.refreshVisibility();
32207 if (isPublic && !prefersReducedMotion() && this.visibilityBtn) {
32208 this.visibilityBtn.animate?.(
32209 [
32210 { transform: "scale(1)" },
32211 { transform: "scale(1.25)" },
32212 { transform: "scale(1)" }
32213 ],
32214 { duration: 300, easing: "ease-out" }
32215 );
32216 }
32217 this.layer.announce(
32218 isPublic ? __("Note is now public.", "desktop-mode") : __("Note is now private.", "desktop-mode")
32219 );
32220 if (this.note.id > 0) {
32221 this.queuePatch({ public: isPublic });
32222 }
32223 }
32224 /**
32225 * All PATCHes flow through one chain so the concurrency token is
32226 * always the latest server-issued one, even when a text save and
32227 * a position save race.
32228 */
32229 queuePatch(body) {
32230 this.patchChain = this.patchChain.then(async () => {
32231 if (this.disposed || this.note.id <= 0) {
32232 return;
32233 }
32234 try {
32235 const saved = await updateNote(this.note.id, {
32236 ...body,
32237 updatedAtMs: this.note.updatedAtMs
32238 });
32239 if (this.disposed) {
32240 return;
32241 }
32242 this.note = {
32243 ...this.note,
32244 updatedAtMs: saved.updatedAtMs,
32245 public: saved.public
32246 };
32247 this.setPhase("saved");
32248 } catch (err) {
32249 if (this.disposed) {
32250 return;
32251 }
32252 if (isNotesConflict(err) && err.current) {
32253 this.pendingText = null;
32254 this.replace(err.current);
32255 this.setPhase("idle");
32256 this.layer.notifyError(
32257 __("This note was changed in another session — showing the latest version.", "desktop-mode")
32258 );
32259 return;
32260 }
32261 this.setPhase("failed");
32262 console.error("[desktop-mode] notes: save failed:", err);
32263 }
32264 });
32265 }
32266 // ------------------------------------------------------------------
32267 // Drag (owner only)
32268 // ------------------------------------------------------------------
32269 startDrag(event) {
32270 if (!this.note.canEdit || this.note.id <= 0) {
32271 return;
32272 }
32273 const dragManager = getDragManager$1();
32274 if (!dragManager) {
32275 return;
32276 }
32277 event.preventDefault();
32278 this.element.ownerDocument.defaultView?.getSelection()?.removeAllRanges();
32279 const ghost = this.buildGhost();
32280 const noteRect = this.element.getBoundingClientRect();
32281 const data = {
32282 noteId: this.note.id,
32283 canEdit: this.note.canEdit,
32284 updatedAtMs: this.note.updatedAtMs
32285 };
32286 const session = dragManager.start({
32287 payload: {
32288 type: NOTE_PAYLOAD_TYPE,
32289 source: this.element,
32290 data,
32291 ghost: {
32292 element: ghost.root,
32293 // The needle tip rides exactly under the cursor —
32294 // the user is holding the pin.
32295 offsetX: ghost.tipX,
32296 offsetY: ghost.tipY,
32297 hint: {
32298 neutral: __("Drop on the desktop to pin", "desktop-mode"),
32299 accept: __("Pin here", "desktop-mode"),
32300 reject: __("Can’t pin here", "desktop-mode")
32301 }
32302 }
32303 },
32304 origin: event,
32305 onClickOnly: () => {
32306 this.teardownDragListeners();
32307 this.toggleMoveMode();
32308 },
32309 onCancel: () => {
32310 this.teardownDragListeners();
32311 void this.snapBack(noteRect);
32312 },
32313 onCommit: () => {
32314 this.teardownDragListeners();
32315 if (this.layer.has(this.note.id)) {
32316 void this.playInsertion(0.7);
32317 }
32318 }
32319 });
32320 if (!session) {
32321 return;
32322 }
32323 this.installDragListeners(ghost);
32324 }
32325 buildGhost() {
32326 const noteRect = this.element.getBoundingClientRect();
32327 const pinImg = this.pinEl.querySelector("img");
32328 const pinRect = (pinImg ?? this.pinEl).getBoundingClientRect();
32329 const tipX = pinRect.left - noteRect.left + pinRect.width * PIN_TIP_X;
32330 const tipY = pinRect.top - noteRect.top + pinRect.height * PIN_TIP_Y;
32331 const root = document.createElement("div");
32332 root.className = "desktop-mode-pinned-note-ghost";
32333 root.style.width = `${noteRect.width}px`;
32334 root.style.height = `${noteRect.height}px`;
32335 const swing = document.createElement("div");
32336 swing.className = "desktop-mode-pinned-note-ghost__swing";
32337 swing.style.transformOrigin = `${tipX}px ${tipY}px`;
32338 const pin = this.pinEl.cloneNode(true);
32339 pin.removeAttribute("aria-pressed");
32340 pin.setAttribute("aria-hidden", "true");
32341 const paper = this.paperEl.cloneNode(true);
32342 paper.classList.add("desktop-mode-pinned-note-ghost__paper");
32343 swing.dataset.noteColor = this.element.dataset.noteColor ?? "";
32344 swing.append(pin, paper);
32345 root.appendChild(swing);
32346 return { root, swing, pin, paper, tipX, tipY };
32347 }
32348 installDragListeners(ghost) {
32349 this.teardownDragListeners();
32350 let pendulum = null;
32351 const onStart = (ev) => {
32352 const detail = ev.detail;
32353 if (detail?.payload?.data?.noteId !== this.note.id) {
32354 return;
32355 }
32356 pendulum = startPendulum(ghost.swing);
32357 };
32358 const onMove = (ev) => {
32359 const detail = ev.detail;
32360 this.lastPointer = { x: detail.clientX, y: detail.clientY };
32361 pendulum?.onPointerMove(detail.clientX);
32362 };
32363 const onEnter = (ev) => {
32364 const detail = ev.detail;
32365 if (detail?.targetId?.startsWith("recycle-bin")) {
32366 ghost.root.classList.add("desktop-mode-pinned-note-ghost--doom");
32367 pendulum?.setBias(6);
32368 }
32369 };
32370 const onLeave = (ev) => {
32371 const detail = ev.detail;
32372 if (detail?.targetId?.startsWith("recycle-bin")) {
32373 ghost.root.classList.remove("desktop-mode-pinned-note-ghost--doom");
32374 pendulum?.setBias(0);
32375 }
32376 };
32377 document.addEventListener(DRAG_EVENTS.START, onStart);
32378 document.addEventListener(DRAG_EVENTS.MOVE, onMove);
32379 document.addEventListener(DRAG_EVENTS.ENTER, onEnter);
32380 document.addEventListener(DRAG_EVENTS.LEAVE, onLeave);
32381 this.dragCleanup = () => {
32382 pendulum?.stop();
32383 pendulum = null;
32384 document.removeEventListener(DRAG_EVENTS.START, onStart);
32385 document.removeEventListener(DRAG_EVENTS.MOVE, onMove);
32386 document.removeEventListener(DRAG_EVENTS.ENTER, onEnter);
32387 document.removeEventListener(DRAG_EVENTS.LEAVE, onLeave);
32388 };
32389 }
32390 teardownDragListeners() {
32391 this.dragCleanup?.();
32392 this.dragCleanup = null;
32393 }
32394 async snapBack(homeRect) {
32395 if (!this.lastPointer || prefersReducedMotion()) {
32396 void this.playInsertion(0.63);
32397 return;
32398 }
32399 const flyback = this.buildGhost();
32400 flyback.root.classList.add("desktop-mode-pinned-note-ghost--flyback");
32401 flyback.root.style.position = "fixed";
32402 flyback.root.style.left = `${this.lastPointer.x - flyback.tipX}px`;
32403 flyback.root.style.top = `${this.lastPointer.y - flyback.tipY}px`;
32404 flyback.root.style.zIndex = "2147483647";
32405 flyback.root.style.pointerEvents = "none";
32406 document.body.appendChild(flyback.root);
32407 try {
32408 await playSnapBack({
32409 flyback: flyback.root,
32410 swing: flyback.swing,
32411 homeX: homeRect.left,
32412 homeY: homeRect.top
32413 });
32414 } finally {
32415 flyback.root.remove();
32416 }
32417 void this.playInsertion(0.63);
32418 }
32419 /** Crumple visual played by the bin drop handler at the release point. */
32420 async playCrumpleAt(clientX, clientY) {
32421 const ghost = this.buildGhost();
32422 ghost.root.classList.add("desktop-mode-pinned-note-ghost--flyback");
32423 ghost.root.style.position = "fixed";
32424 ghost.root.style.left = `${clientX - ghost.tipX}px`;
32425 ghost.root.style.top = `${clientY - ghost.tipY}px`;
32426 ghost.root.style.zIndex = "2147483647";
32427 ghost.root.style.pointerEvents = "none";
32428 document.body.appendChild(ghost.root);
32429 try {
32430 await playCrumpleIntoBin({
32431 clone: ghost.root,
32432 pin: ghost.pin,
32433 paper: ghost.paper,
32434 binX: clientX,
32435 binY: clientY + 24
32436 });
32437 } finally {
32438 ghost.root.remove();
32439 }
32440 }
32441 // ------------------------------------------------------------------
32442 // Keyboard move mode
32443 // ------------------------------------------------------------------
32444 toggleMoveMode() {
32445 if (this.moveMode) {
32446 this.exitMoveMode(true);
32447 } else {
32448 this.enterMoveMode();
32449 }
32450 }
32451 enterMoveMode() {
32452 if (!this.note.canEdit) {
32453 return;
32454 }
32455 this.moveMode = true;
32456 this.moveOrigin = { x: this.note.x, y: this.note.y };
32457 this.element.classList.add("desktop-mode-pinned-note--move-mode");
32458 this.pinEl.setAttribute("aria-pressed", "true");
32459 this.layer.announce(
32460 __(
32461 "Moving note. Arrow keys to move, Enter to place, Escape to cancel, Delete to move to the Recycle Bin.",
32462 "desktop-mode"
32463 )
32464 );
32465 }
32466 exitMoveMode(commit) {
32467 if (!this.moveMode) {
32468 return;
32469 }
32470 this.moveMode = false;
32471 this.element.classList.remove("desktop-mode-pinned-note--move-mode");
32472 this.pinEl.setAttribute("aria-pressed", "false");
32473 if (commit) {
32474 this.moveTo(this.note.x, this.note.y);
32475 this.layer.announce(__("Note placed.", "desktop-mode"));
32476 void this.playInsertion(0.7);
32477 } else if (this.moveOrigin) {
32478 this.note = { ...this.note, ...this.moveOrigin };
32479 this.applyPosition();
32480 }
32481 this.moveOrigin = null;
32482 }
32483 onPinKeydown(event) {
32484 if (!this.moveMode) {
32485 return;
32486 }
32487 const { width, height } = this.layer.hostSize();
32488 const step = event.shiftKey ? KEYBOARD_FINE_STEP_PX : KEYBOARD_STEP_PX;
32489 const dx = step / width;
32490 const dy = step / height;
32491 switch (event.key) {
32492 case "ArrowLeft":
32493 this.nudge(-dx, 0);
32494 break;
32495 case "ArrowRight":
32496 this.nudge(dx, 0);
32497 break;
32498 case "ArrowUp":
32499 this.nudge(0, -dy);
32500 break;
32501 case "ArrowDown":
32502 this.nudge(0, dy);
32503 break;
32504 case "Enter":
32505 case " ":
32506 this.exitMoveMode(true);
32507 break;
32508 case "Escape":
32509 this.exitMoveMode(false);
32510 this.layer.announce(__("Move cancelled.", "desktop-mode"));
32511 break;
32512 case "Delete":
32513 case "Backspace":
32514 this.exitMoveMode(false);
32515 void wpdConfirm({
32516 title: __("Move note to the Recycle Bin?", "desktop-mode"),
32517 message: __("You can restore it from the Recycle Bin later.", "desktop-mode"),
32518 confirmLabel: __("Move to Trash", "desktop-mode"),
32519 danger: true
32520 }).then((confirmed) => {
32521 if (confirmed) {
32522 this.layer.trashNote(this.note);
32523 }
32524 });
32525 break;
32526 default:
32527 return;
32528 }
32529 event.preventDefault();
32530 event.stopPropagation();
32531 }
32532 nudge(dx, dy) {
32533 const clamped = this.layer.clampPosition(this.note.x + dx, this.note.y + dy);
32534 this.note = { ...this.note, ...clamped };
32535 this.applyPosition();
32536 }
32537 }
32538 const handlers = /* @__PURE__ */ new Map();
32539 function registerRecycleBinPayloadHandler(type, handler) {
32540 handlers.set(type, handler);
32541 return () => {
32542 if (handlers.get(type) === handler) {
32543 handlers.delete(type);
32544 }
32545 };
32546 }
32547 function recycleBinPayloadAccepts(payload) {
32548 const handler = handlers.get(payload.type);
32549 return handler ? handler.accept(payload.data) : false;
32550 }
32551 function recycleBinPayloadDrop(session, ev) {
32552 const handler = handlers.get(session.payload.type);
32553 if (!handler) {
32554 return false;
32555 }
32556 handler.onDrop(session, ev);
32557 return true;
32558 }
32559 function normalizedDropPosition(layer, session, ev) {
32560 const rect = layer.host.getBoundingClientRect();
32561 const offsetX = session.payload.ghost?.offsetX ?? 0;
32562 const offsetY = session.payload.ghost?.offsetY ?? 0;
32563 const { width, height } = layer.hostSize();
32564 return layer.clampPosition(
32565 (ev.clientX - offsetX - rect.left) / width,
32566 (ev.clientY - offsetY - rect.top) / height
32567 );
32568 }
32569 function handleDraftDrop(layer, session, ev) {
32570 const data = session.payload.data;
32571 const text = String(data.text ?? "");
32572 if (!text.trim()) {
32573 return;
32574 }
32575 const { x, y } = normalizedDropPosition(layer, session, ev);
32576 const color = sanitizeNoteColorSlug(String(data.color ?? ""));
32577 const isPublic = data.isPublic === true;
32578 const seed2 = hashNoteSeed(text);
32579 const tempId = layer.nextTempId();
32580 const optimistic = {
32581 id: tempId,
32582 text,
32583 color,
32584 x,
32585 y,
32586 z: 1,
32587 public: isPublic,
32588 seed: seed2,
32589 ownerId: 0,
32590 ownerName: "",
32591 ownerAvatar: "",
32592 canEdit: true,
32593 updatedAtMs: 0
32594 };
32595 const controller = layer.upsertNote(optimistic, { animate: "thunk" });
32596 layer.bringToFront(controller);
32597 void createNote({ text, color, x, y, public: isPublic, seed: seed2 }).then((note) => {
32598 layer.bumpHighWater(note.updatedAtMs);
32599 controller.replace(note);
32600 layer.rekeyNote(tempId, controller);
32601 controller.flushPendingEdits();
32602 }).catch((err) => {
32603 layer.removeNote(tempId);
32604 layer.notifyError(
32605 __("Could not pin the note. Please try again.", "desktop-mode")
32606 );
32607 console.error("[desktop-mode] notes: create failed:", err);
32608 });
32609 }
32610 function handleNoteDrop(layer, session, ev) {
32611 const data = session.payload.data;
32612 const controller = layer.get(data.noteId);
32613 if (!controller) {
32614 return;
32615 }
32616 const { x, y } = normalizedDropPosition(layer, session, ev);
32617 controller.moveTo(x, y);
32618 }
32619 function installNoteDropHandlers(layer) {
32620 const deregisters = [];
32621 const canvasCtxOk = (ctx) => ctx.folderId === 0;
32622 deregisters.push(
32623 registerCanvasPayloadHandler(NOTE_DRAFT_PAYLOAD_TYPE, {
32624 accept: (data, ctx) => canvasCtxOk(ctx) && Boolean(String(data.text ?? "").trim()),
32625 onDrop: (session, ev) => handleDraftDrop(layer, session, ev)
32626 })
32627 );
32628 deregisters.push(
32629 registerCanvasPayloadHandler(NOTE_PAYLOAD_TYPE, {
32630 accept: (data, ctx) => canvasCtxOk(ctx) && data.canEdit === true,
32631 onDrop: (session, ev) => handleNoteDrop(layer, session, ev)
32632 })
32633 );
32634 deregisters.push(
32635 registerRecycleBinPayloadHandler(NOTE_PAYLOAD_TYPE, {
32636 accept: (data) => data.canEdit === true,
32637 onDrop: (session, ev) => {
32638 const data = session.payload.data;
32639 const controller = layer.get(data.noteId);
32640 if (!controller) {
32641 return;
32642 }
32643 const note = controller.note;
32644 void controller.playCrumpleAt(ev.clientX, ev.clientY);
32645 layer.trashNote(note);
32646 }
32647 })
32648 );
32649 return () => {
32650 deregisters.forEach((deregister) => deregister());
32651 };
32652 }
32653 const DROP_ACTIVE_ATTR = "data-desktop-mode-posts-drop-active";
32654 const POSTS_WINDOW_ID = "desktop-mode-posts";
32655 const POSTS_DOCK_SELECTOR = '.desktop-mode-dock__item[data-menu-slug="menu-posts"],.desktop-mode-dock__item[data-menu-slug="editphp"]';
32656 const POSTS_WINDOW_SELECTOR = "[data-desktop-mode-posts-root]";
32657 function getDragManager() {
32658 return window.wp?.desktop?.dragManager ?? null;
32659 }
32660 function isNotePayload(payload) {
32661 if (payload.type !== NOTE_PAYLOAD_TYPE) {
32662 return false;
32663 }
32664 const data = payload.data;
32665 return data.canEdit === true;
32666 }
32667 function isPostsUrl(url) {
32668 if (!url) {
32669 return false;
32670 }
32671 let path = url;
32672 let search = "";
32673 try {
32674 const parsed = new URL(url, window.location.origin);
32675 path = parsed.pathname;
32676 search = parsed.search;
32677 } catch {
32678 }
32679 const onPostsScreen = /(?:^|\/)(?:edit\.php|post-new\.php)$/.test(path) || !path.includes("/") && (path === "edit.php" || path === "post-new.php");
32680 if (!onPostsScreen) {
32681 return false;
32682 }
32683 const postType = new URLSearchParams(search).get("post_type");
32684 return !postType || postType === "post";
32685 }
32686 function isPostsShortcutTile(ctx) {
32687 const file = ctx.placement.file;
32688 if (!file || file.type !== "shortcut") {
32689 return false;
32690 }
32691 const url = typeof file.shortcutUrl === "string" ? file.shortcutUrl : "";
32692 return isPostsUrl(url);
32693 }
32694 function convertDraggedNote(layer, session) {
32695 if (session.payload.type !== NOTE_PAYLOAD_TYPE) {
32696 return;
32697 }
32698 const data = session.payload.data;
32699 if (typeof data.noteId !== "number") {
32700 return;
32701 }
32702 const controller = layer.get(data.noteId);
32703 if (controller) {
32704 layer.convertNote(controller.note);
32705 }
32706 }
32707 let _installed$1 = false;
32708 let _dockDeregister$1 = null;
32709 let _windowDeregister$1 = null;
32710 let _mutationObserver = null;
32711 function registerOn$1(dragManager, layer, id, el) {
32712 return dragManager.registerDropTarget({
32713 id,
32714 element: el,
32715 acceptLabel: __("Convert to post", "desktop-mode"),
32716 accept: (payload) => isNotePayload(payload),
32717 onEnter: () => {
32718 el.setAttribute(DROP_ACTIVE_ATTR, "");
32719 },
32720 onLeave: () => {
32721 el.removeAttribute(DROP_ACTIVE_ATTR);
32722 },
32723 onDrop: (session) => {
32724 el.removeAttribute(DROP_ACTIVE_ATTR);
32725 convertDraggedNote(layer, session);
32726 }
32727 });
32728 }
32729 function registeredElement(dragManager, id) {
32730 const t = dragManager.debug().listTargets().find((target2) => target2.id === id);
32731 return t ? t.element : null;
32732 }
32733 function installNotesPostsDropTarget(layer) {
32734 if (_installed$1 || !layer.canCreatePosts) {
32735 return;
32736 }
32737 const dragManager = getDragManager();
32738 if (!dragManager) {
32739 return;
32740 }
32741 _installed$1 = true;
32742 registerTilePayloadHandler(NOTE_PAYLOAD_TYPE, {
32743 appliesTo: (ctx) => isPostsShortcutTile(ctx),
32744 acceptLabel: __("Convert to post", "desktop-mode"),
32745 accept: (data) => data.canEdit === true,
32746 onDrop: (session) => convertDraggedNote(layer, session)
32747 });
32748 const reprobeTile = () => {
32749 const el = document.querySelector(POSTS_DOCK_SELECTOR);
32750 if (!(el instanceof HTMLElement)) {
32751 _dockDeregister$1?.();
32752 _dockDeregister$1 = null;
32753 return;
32754 }
32755 if (_dockDeregister$1 && registeredElement(dragManager, "notes-convert-dock") === el) {
32756 return;
32757 }
32758 _dockDeregister$1?.();
32759 _dockDeregister$1 = registerOn$1(dragManager, layer, "notes-convert-dock", el);
32760 };
32761 reprobeTile();
32762 addAction(
32763 HOOKS.DOCK_AFTER_RENDER,
32764 "desktop-mode/notes/convert-dock-target",
32765 reprobeTile
32766 );
32767 if (typeof MutationObserver !== "undefined") {
32768 _mutationObserver = new MutationObserver(() => {
32769 reprobeTile();
32770 });
32771 _mutationObserver.observe(document.body, {
32772 childList: true,
32773 subtree: true
32774 });
32775 }
32776 addAction(
32777 HOOKS.WINDOW_OPENED,
32778 "desktop-mode/notes/convert-window-target",
32779 (detail) => {
32780 if (detail.windowId !== POSTS_WINDOW_ID) {
32781 return;
32782 }
32783 _windowDeregister$1?.();
32784 _windowDeregister$1 = null;
32785 const el = document.querySelector(POSTS_WINDOW_SELECTOR);
32786 if (el instanceof HTMLElement) {
32787 _windowDeregister$1 = registerOn$1(
32788 dragManager,
32789 layer,
32790 "notes-convert-window",
32791 el
32792 );
32793 }
32794 }
32795 );
32796 addAction(
32797 HOOKS.WINDOW_CLOSED,
32798 "desktop-mode/notes/convert-window-cleanup",
32799 (detail) => {
32800 if (detail.windowId !== POSTS_WINDOW_ID) {
32801 return;
32802 }
32803 _windowDeregister$1?.();
32804 _windowDeregister$1 = null;
32805 }
32806 );
32807 }
32808 function bootNotes(options) {
32809 const notesUrl = options.config.notesUrl;
32810 if (typeof notesUrl !== "string" || !notesUrl) {
32811 return null;
32812 }
32813 installNotesRestDeps({
32814 baseUrl: notesUrl,
32815 nonce: options.config.restNonce ?? ""
32816 });
32817 const layer = new NotesLayer({
32818 host: options.host,
32819 pluginUrl: options.config.pluginUrl ?? "",
32820 canCreatePosts: options.config.canCreatePosts ?? false,
32821 onError: options.onError
32822 });
32823 installNoteDropHandlers(layer);
32824 installNotesPostsDropTarget(layer);
32825 document.addEventListener(NOTE_CREATED_EVENT, (ev) => {
32826 const note = ev.detail?.note;
32827 if (note && typeof note.id === "number") {
32828 layer.bumpHighWater(note.updatedAtMs);
32829 const controller = layer.upsertNote(note, { animate: "thunk" });
32830 layer.bringToFront(controller);
32831 controller.focusEditor();
32832 }
32833 });
32834 void layer.boot();
32835 return layer;
32836 }
32837 const clock = {
32838 id: "clock",
32839 // Labels/descriptions on built-in defs stay string-literal at
32840 // module-eval time so the extract-pot pass picks them up. The
32841 // values are wrapped in `__()` so they translate at runtime.
32842 get label() {
32843 return __("Clock");
32844 },
32845 get description() {
32846 return __("Local time and date, refreshed every second.");
32847 },
32848 icon: "dashicons-clock",
32849 mount: (container) => {
32850 container.classList.add("desktop-mode-widget-clock");
32851 const time = document.createElement("div");
32852 time.className = "desktop-mode-widget-clock__time";
32853 container.appendChild(time);
32854 const date = document.createElement("div");
32855 date.className = "desktop-mode-widget-clock__date";
32856 container.appendChild(date);
32857 const render2 = () => {
32858 const now = /* @__PURE__ */ new Date();
32859 time.textContent = now.toLocaleTimeString(void 0, {
32860 hour: "2-digit",
32861 minute: "2-digit"
32862 });
32863 date.textContent = now.toLocaleDateString(void 0, {
32864 weekday: "long",
32865 month: "short",
32866 day: "numeric"
32867 });
32868 };
32869 render2();
32870 const msUntilNextSecond = 1e3 - Date.now() % 1e3;
32871 let interval = null;
32872 const kickoff = window.setTimeout(() => {
32873 render2();
32874 interval = window.setInterval(render2, 1e3);
32875 }, msUntilNextSecond);
32876 return () => {
32877 window.clearTimeout(kickoff);
32878 if (interval !== null) {
32879 window.clearInterval(interval);
32880 }
32881 };
32882 }
32883 };
32884 function registerBuiltInWidgets() {
32885 register(clock);
32886 }
32887 const STYLE_ID = "desktop-mode-release-card-styles";
32888 const HOST_CLASS = "desktop-mode-release-host";
32889 const WP_LOGO = '<svg viewBox="0 0 122.52 122.523" aria-hidden="true"><path fill="currentColor" d="M8.708 61.26c0 20.802 12.089 38.779 29.619 47.298L13.258 39.872a52.352 52.352 0 0 0-4.55 21.388zm87.892-2.652c0-6.495-2.333-10.993-4.334-14.494-2.664-4.329-5.161-7.995-5.161-12.324 0-4.831 3.664-9.328 8.825-9.328.233 0 .454.029.681.042-9.35-8.566-21.807-13.796-35.489-13.796-18.36 0-34.513 9.42-43.91 23.688 1.233.037 2.395.063 3.382.063 5.497 0 14.006-.667 14.006-.667 2.833-.167 3.167 3.994.337 4.329 0 0-2.847.335-6.015.501l19.138 56.925 11.502-34.493-8.187-22.432c-2.831-.166-5.51-.501-5.51-.501-2.831-.167-2.499-4.496.332-4.329 0 0 8.679.667 13.843.667 5.496 0 14.006-.667 14.006-.667 2.835-.167 3.168 3.994.337 4.329 0 0-2.852.335-6.015.501l18.992 56.494 5.242-17.517c2.272-7.269 4.001-12.49 4.001-16.989zm-34.404 7.223l-15.768 45.819a52.552 52.552 0 0 0 14.807 2.136c6.309 0 12.36-1.091 17.996-3.075a4.617 4.617 0 0 1-.374-.724L62.196 65.831zm45.192-29.81c.226 1.674.354 3.471.354 5.404 0 5.333-.996 11.328-3.996 18.824l-16.053 46.413c15.624-9.111 26.133-26.038 26.133-45.426.001-9.137-2.333-17.729-6.438-25.215zM61.262 0C27.483 0 0 27.481 0 61.26c0 33.783 27.483 61.263 61.262 61.263 33.778 0 61.265-27.48 61.265-61.263C122.526 27.481 95.04 0 61.262 0zm0 119.715c-32.23 0-58.453-26.223-58.453-58.455 0-32.23 26.222-58.451 58.453-58.451 32.229 0 58.45 26.221 58.45 58.451 0 32.232-26.221 58.455-58.45 58.455z"/></svg>';
32890 const CLOSE_ICON = '<svg viewBox="0 0 14 14" aria-hidden="true"><path d="M3 3 L11 11 M11 3 L3 11" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" fill="none"></path></svg>';
32891 const STYLES = `
32892 .dm-release-card {
32893 position: relative; box-sizing: border-box; width: 268px; padding: 11px;
32894 border-radius: 14px; color: #fff;
32895 font-family: var( --desktop-mode-font, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif );
32896 background: #34373f; border: 1px solid rgba( 255, 255, 255, 0.14 );
32897 box-shadow: 0 16px 40px rgba( 0, 0, 0, 0.55 ), 0 3px 8px rgba( 0, 0, 0, 0.3 ), inset 0 0 0 1px rgba( 255, 255, 255, 0.04 );
32898 --accent: #2271b1; --accent-ink: #ffffff;
32899 animation: dmRcCardIn 0.5s cubic-bezier( 0.2, 1.2, 0.35, 1 ) both;
32900 }
32901 .dm-release-card, .dm-release-card * { box-sizing: border-box; }
32902 @keyframes dmRcCardIn {
32903 from { opacity: 0; transform: translateY( -16px ) scale( 0.96 ); }
32904 to { opacity: 1; transform: none; }
32905 }
32906 .dm-rc__close {
32907 position: absolute; top: 9px; right: 9px; z-index: 10;
32908 width: 22px; height: 22px; padding: 0; border: none; border-radius: 50%;
32909 display: inline-flex; align-items: center; justify-content: center;
32910 background: rgba( 0, 0, 0, 0.5 ); color: #fff; opacity: 0.72; cursor: pointer;
32911 transition: opacity 0.12s ease, background-color 0.12s ease;
32912 }
32913 .dm-rc__close:hover { opacity: 1; background: rgba( 0, 0, 0, 0.7 ); }
32914 .dm-rc__close:focus-visible { opacity: 1; outline: 2px solid #fff; outline-offset: 2px; }
32915 .dm-rc__close svg { width: 11px; height: 11px; }
32916 .dm-rc__art { position: relative; height: 150px; }
32917 .dm-rc__cover {
32918 position: absolute; left: 2px; top: 0; width: 150px; height: 150px;
32919 border-radius: 2px; overflow: hidden; z-index: 3;
32920 box-shadow: 0 8px 20px rgba( 0, 0, 0, 0.5 ), inset 0 0 0 1px rgba( 255, 255, 255, 0.08 );
32921 }
32922 .dm-rc__canvas { width: 100%; height: 100%; display: block; }
32923 .dm-rc__disc-wrap {
32924 position: absolute; left: 94px; top: 2px; width: 148px; height: 148px; z-index: 2;
32925 border-radius: 50%; box-shadow: 0 14px 26px rgba( 0, 0, 0, 0.6 );
32926 animation: dmRcEmerge 0.8s cubic-bezier( 0.2, 1, 0.28, 1 ) 0.45s both;
32927 }
32928 @keyframes dmRcEmerge {
32929 from { transform: translateX( -84px ); }
32930 to { transform: translateX( 0 ); }
32931 }
32932 .dm-rc__disc {
32933 position: absolute; inset: 0; border-radius: 50%;
32934 background:
32935 repeating-radial-gradient( circle at 50% 50%, rgba( 255, 255, 255, 0.05 ) 0 1px, rgba( 0, 0, 0, 0 ) 1px 2.4px ),
32936 radial-gradient( circle at 50% 50%, #1a1a1e 0 11%, #0a0a0c 12% 62%, #050506 100% );
32937 box-shadow: inset 0 0 26px rgba( 0, 0, 0, 0.9 ), inset 0 0 0 1px rgba( 255, 255, 255, 0.05 );
32938 animation: dmRcSettle 2.5s cubic-bezier( 0.12, 0.72, 0.16, 1 ) 0.45s both;
32939 }
32940 @keyframes dmRcSettle {
32941 from { transform: rotate( 0 ); }
32942 to { transform: rotate( 720deg ); }
32943 }
32944 .dm-rc__label {
32945 position: absolute; inset: 34%; border-radius: 50%; display: grid; place-items: center;
32946 background: var( --accent ); color: var( --accent-ink );
32947 box-shadow: inset 0 0 0 2px rgba( 0, 0, 0, 0.18 ), 0 1px 2px rgba( 0, 0, 0, 0.4 );
32948 }
32949 .dm-rc__label svg { width: 59%; height: 59%; display: block; }
32950 .dm-rc__sheen {
32951 position: absolute; inset: 0; border-radius: 50%; pointer-events: none; z-index: 3;
32952 background: linear-gradient( 118deg, rgba( 255, 255, 255, 0.18 ) 0%, transparent 24%, transparent 74%, rgba( 255, 255, 255, 0.1 ) 100% );
32953 mix-blend-mode: screen;
32954 }
32955 .dm-rc__meta {
32956 display: flex; align-items: center; gap: 10px; margin-top: 11px;
32957 opacity: 0; animation: dmRcFade 0.5s ease 1.05s forwards;
32958 }
32959 @keyframes dmRcFade { to { opacity: 1; } }
32960 .dm-rc__text { flex: 1; font-size: 13px; line-height: 1.35; color: #fff; }
32961 .dm-rc__text b { font-weight: 650; }
32962 .dm-rc__btn {
32963 flex-shrink: 0; padding: 7px 12px; border: none; border-radius: 7px;
32964 color: var( --accent-ink ); background: var( --accent ); font: inherit; font-size: 12px; font-weight: 600;
32965 cursor: pointer; box-shadow: 0 2px 8px rgba( 0, 0, 0, 0.3 ); transition: filter 0.12s;
32966 }
32967 .dm-rc__btn:hover { filter: brightness( 1.12 ); }
32968 .dm-rc__btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
32969 @media ( prefers-reduced-motion: reduce ) {
32970 .dm-release-card, .dm-rc__disc-wrap, .dm-rc__disc, .dm-rc__meta { animation: none !important; }
32971 .dm-rc__disc-wrap { transform: translateX( 0 ); }
32972 .dm-rc__meta { opacity: 1; }
32973 }
32974 `;
32975 function ensureStyles() {
32976 if (document.getElementById(STYLE_ID)) {
32977 return;
32978 }
32979 const el = document.createElement("style");
32980 el.id = STYLE_ID;
32981 el.textContent = STYLES;
32982 document.head.appendChild(el);
32983 }
32984 function ensureHost() {
32985 const existing = document.querySelector("." + HOST_CLASS);
32986 if (existing) {
32987 return existing;
32988 }
32989 const el = document.createElement("div");
32990 el.className = HOST_CLASS;
32991 el.style.cssText = "position:fixed;top:calc(var(--wp-admin--admin-bar--height,32px) + 16px);inset-inline-end:16px;z-index:calc(var(--desktop-mode-z-fullscreen,99999) + 10);pointer-events:none;";
32992 document.body.appendChild(el);
32993 return el;
32994 }
32995 function paintSleeve(root, canvas, artUrl, hasExplicitAccent) {
32996 const img = new Image();
32997 img.crossOrigin = "anonymous";
32998 img.addEventListener(
32999 "load",
33000 () => {
33001 const w = img.naturalWidth || 0;
33002 const h = img.naturalHeight || 0;
33003 if (!w || !h) {
33004 return;
33005 }
33006 const size = 320;
33007 canvas.width = size;
33008 canvas.height = size;
33009 const ctx = canvas.getContext("2d");
33010 if (!ctx) {
33011 return;
33012 }
33013 const baseSide = Math.min(w, h);
33014 ctx.drawImage(img, 0, 0, baseSide, baseSide, 0, 0, size, size);
33015 try {
33016 const work = document.createElement("canvas");
33017 work.width = w;
33018 work.height = h;
33019 const wctx = work.getContext("2d");
33020 if (!wctx) {
33021 return;
33022 }
33023 wctx.drawImage(img, 0, 0);
33024 const data = wctx.getImageData(0, 0, w, h).data;
33025 const isWhite = (x, y) => {
33026 const i = (y * w + x) * 4;
33027 return data[i] > 248 && data[i + 1] > 248 && data[i + 2] > 248 && data[i + 3] > 200;
33028 };
33029 const rowWhite = (y) => {
33030 for (let x = 0; x < w; x += 2) {
33031 if (!isWhite(x, y)) {
33032 return false;
33033 }
33034 }
33035 return true;
33036 };
33037 const colWhite = (x) => {
33038 for (let y = 0; y < h; y += 2) {
33039 if (!isWhite(x, y)) {
33040 return false;
33041 }
33042 }
33043 return true;
33044 };
33045 let top = 0;
33046 while (top < h - 1 && rowWhite(top)) {
33047 top++;
33048 }
33049 let bottom = h - 1;
33050 while (bottom > top && rowWhite(bottom)) {
33051 bottom--;
33052 }
33053 let left = 0;
33054 while (left < w - 1 && colWhite(left)) {
33055 left++;
33056 }
33057 let right = w - 1;
33058 while (right > left && colWhite(right)) {
33059 right--;
33060 }
33061 const side = Math.max(1, Math.min(right - left + 1, bottom - top + 1));
33062 ctx.clearRect(0, 0, size, size);
33063 ctx.drawImage(img, left, top, side, side, 0, 0, size, size);
33064 if (!hasExplicitAccent) {
33065 extractAccent(root, ctx, size);
33066 }
33067 } catch {
33068 }
33069 },
33070 { once: true }
33071 );
33072 img.src = artUrl;
33073 }
33074 function extractAccent(root, ctx, size) {
33075 const { data } = ctx.getImageData(0, 0, size, size);
33076 const buckets = /* @__PURE__ */ new Map();
33077 let best = null;
33078 let bestScore = -1;
33079 for (let i = 0; i < data.length; i += 4) {
33080 const r2 = data[i];
33081 const g2 = data[i + 1];
33082 const b2 = data[i + 2];
33083 if (data[i + 3] < 200) {
33084 continue;
33085 }
33086 const max = Math.max(r2, g2, b2);
33087 const min = Math.min(r2, g2, b2);
33088 const v = max / 255;
33089 const s = max === 0 ? 0 : (max - min) / max;
33090 if (v < 0.2 || s < 0.25) {
33091 continue;
33092 }
33093 const key = `${Math.floor(r2 / 16)},${Math.floor(g2 / 16)},${Math.floor(b2 / 16)}`;
33094 let bucket2 = buckets.get(key);
33095 if (!bucket2) {
33096 bucket2 = { r: 0, g: 0, b: 0, n: 0, score: 0 };
33097 buckets.set(key, bucket2);
33098 }
33099 bucket2.r += r2;
33100 bucket2.g += g2;
33101 bucket2.b += b2;
33102 bucket2.n += 1;
33103 bucket2.score += s * v;
33104 if (bucket2.score > bestScore) {
33105 bestScore = bucket2.score;
33106 best = bucket2;
33107 }
33108 }
33109 if (!best) {
33110 return;
33111 }
33112 const r = Math.round(best.r / best.n);
33113 const g = Math.round(best.g / best.n);
33114 const b = Math.round(best.b / best.n);
33115 const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
33116 root.style.setProperty("--accent", `rgb(${r}, ${g}, ${b})`);
33117 root.style.setProperty("--accent-ink", lum > 0.6 ? "#1a1a1a" : "#ffffff");
33118 }
33119 function showReleaseCard(opts) {
33120 ensureStyles();
33121 const host = ensureHost();
33122 host.textContent = "";
33123 const root = document.createElement("div");
33124 root.className = "dm-release-card";
33125 root.setAttribute("role", "status");
33126 root.style.pointerEvents = "auto";
33127 if (opts.accent) {
33128 root.style.setProperty("--accent", opts.accent);
33129 }
33130 if (opts.accentInk) {
33131 root.style.setProperty("--accent-ink", opts.accentInk);
33132 }
33133 root.innerHTML = `<button type="button" class="dm-rc__close">${CLOSE_ICON}</button><div class="dm-rc__art"><div class="dm-rc__disc-wrap"><div class="dm-rc__disc"><div class="dm-rc__label">${WP_LOGO}</div></div><div class="dm-rc__sheen"></div></div><div class="dm-rc__cover"><canvas class="dm-rc__canvas"></canvas></div></div><div class="dm-rc__meta"><span class="dm-rc__text"></span><button type="button" class="dm-rc__btn"></button></div>`;
33134 root.querySelector(".dm-rc__text").textContent = opts.message;
33135 const closeBtn = root.querySelector(".dm-rc__close");
33136 closeBtn.setAttribute("aria-label", __("Dismiss"));
33137 const updateBtn = root.querySelector(".dm-rc__btn");
33138 updateBtn.textContent = __("Update now");
33139 host.appendChild(root);
33140 paintSleeve(
33141 root,
33142 root.querySelector(".dm-rc__canvas"),
33143 opts.artUrl,
33144 !!opts.accent
33145 );
33146 let done = false;
33147 let timer = null;
33148 const removeNow = () => {
33149 done = true;
33150 if (timer !== null) {
33151 clearTimeout(timer);
33152 timer = null;
33153 }
33154 root.remove();
33155 };
33156 closeBtn.addEventListener(
33157 "click",
33158 (e) => {
33159 e.preventDefault();
33160 e.stopPropagation();
33161 if (done) {
33162 return;
33163 }
33164 markNoticeDismissed(opts.dismissKey);
33165 const reduce = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
33166 if (reduce) {
33167 removeNow();
33168 return;
33169 }
33170 done = true;
33171 root.style.animation = "none";
33172 root.style.transition = "opacity 0.2s ease";
33173 requestAnimationFrame(() => {
33174 root.style.opacity = "0";
33175 });
33176 timer = window.setTimeout(() => root.remove(), 240);
33177 }
33178 );
33179 updateBtn.addEventListener("click", (e) => {
33180 e.preventDefault();
33181 e.stopPropagation();
33182 opts.onUpdate();
33183 removeNow();
33184 });
33185 return removeNow;
33186 }
33187 const CACHE_PREFIX = "desktop-mode/release-art:v1:";
33188 const MISS_TTL_MS = 6 * 60 * 60 * 1e3;
33189 function str(v) {
33190 return typeof v === "string" ? v : "";
33191 }
33192 function prop(o, key) {
33193 return o && typeof o === "object" ? o[key] : void 0;
33194 }
33195 function decodeEntities(s) {
33196 const el = document.createElement("textarea");
33197 el.innerHTML = s;
33198 return el.value;
33199 }
33200 function pickMedia(post) {
33201 const media = prop(prop(post, "_embedded"), "wp:featuredmedia");
33202 const first = Array.isArray(media) ? media[0] : void 0;
33203 const sizes = prop(prop(first, "media_details"), "sizes");
33204 for (const key of ["medium_large", "large", "1536x1536", "medium"]) {
33205 const url = str(prop(prop(sizes, key), "source_url"));
33206 if (url) {
33207 return url;
33208 }
33209 }
33210 return str(prop(first, "source_url"));
33211 }
33212 function parseReleaseArt(posts, branch) {
33213 if (!Array.isArray(posts)) {
33214 return null;
33215 }
33216 const escaped = branch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33217 const re = new RegExp(
33218 "^WordPress " + escaped + '\\s*[“"]([^”"]+)[”"]'
33219 );
33220 for (const post of posts) {
33221 const title = decodeEntities(str(prop(prop(post, "title"), "rendered")));
33222 const m = re.exec(title);
33223 if (!m) {
33224 continue;
33225 }
33226 const artUrl = pickMedia(post);
33227 if (artUrl) {
33228 return { name: m[1].trim(), artUrl };
33229 }
33230 }
33231 return null;
33232 }
33233 function readCache(branch) {
33234 try {
33235 const raw = localStorage.getItem(CACHE_PREFIX + branch);
33236 if (!raw) {
33237 return null;
33238 }
33239 const v = JSON.parse(raw);
33240 if (v.ok === true && str(v.name) && str(v.artUrl)) {
33241 return { name: str(v.name), artUrl: str(v.artUrl) };
33242 }
33243 if (v.ok === false && typeof v.ts === "number" && Date.now() - v.ts < MISS_TTL_MS) {
33244 return "miss";
33245 }
33246 return null;
33247 } catch {
33248 return null;
33249 }
33250 }
33251 function writeCache(branch, value) {
33252 try {
33253 localStorage.setItem(CACHE_PREFIX + branch, JSON.stringify(value));
33254 } catch {
33255 }
33256 }
33257 async function resolveReleaseArt(branch) {
33258 if (!branch) {
33259 return null;
33260 }
33261 const cached = readCache(branch);
33262 if (cached === "miss") {
33263 return null;
33264 }
33265 if (cached) {
33266 return cached;
33267 }
33268 try {
33269 const url = "https://wordpress.org/news/wp-json/wp/v2/posts?search=" + encodeURIComponent(branch) + "&per_page=100&_fields=title,_links,_embedded&_embed=wp:featuredmedia";
33270 const res = await trackedFetch$1(
33271 url,
33272 { credentials: "omit" },
33273 { silent: true, source: "desktop-mode/release-art" }
33274 );
33275 if (!res.ok) {
33276 writeCache(branch, { ok: false, ts: Date.now() });
33277 return null;
33278 }
33279 const art = parseReleaseArt(await res.json(), branch);
33280 if (art) {
33281 writeCache(branch, { ok: true, name: art.name, artUrl: art.artUrl });
33282 return art;
33283 }
33284 writeCache(branch, { ok: false, ts: Date.now() });
33285 return null;
33286 } catch {
33287 writeCache(branch, { ok: false, ts: Date.now() });
33288 return null;
33289 }
33290 }
33291 function preloadImage(url, timeoutMs = 5e3) {
33292 return new Promise((resolve2) => {
33293 const img = new Image();
33294 img.crossOrigin = "anonymous";
33295 let done = false;
33296 const finish = (ok) => {
33297 if (done) {
33298 return;
33299 }
33300 done = true;
33301 resolve2(ok);
33302 };
33303 img.addEventListener("load", () => finish(true), { once: true });
33304 img.addEventListener("error", () => finish(false), { once: true });
33305 window.setTimeout(() => finish(false), timeoutMs);
33306 img.src = url;
33307 });
33308 }
33309 function updateMessage(version, name) {
33310 if (name) {
33311 const withName = __('WordPress %1$s "%2$s" is available.');
33312 return sprintf(withName, version, name);
33313 }
33314 const versionOnly = __("WordPress %s is available.");
33315 return sprintf(versionOnly, version);
33316 }
33317 async function maybeShowUpdate(deps2) {
33318 const { update, openUrl } = deps2;
33319 if (!update || typeof update.version !== "string" || !update.version || typeof update.url !== "string" || !update.url) {
33320 return;
33321 }
33322 const version = update.version;
33323 const branch = typeof update.branch === "string" && update.branch ? update.branch : version;
33324 const crossing = update.crossing === true;
33325 const exact = typeof update.available === "string" && update.available ? update.available : version;
33326 const dismissKey = `desktop-mode/core-update:${exact}`;
33327 if (isNoticeDismissed(dismissKey)) {
33328 return;
33329 }
33330 const openUpdateScreen = () => openUrl({ url: update.url, title: __("WordPress Updates") });
33331 const resolveArt = deps2.resolveArt ?? resolveReleaseArt;
33332 const load = deps2.loadImage ?? preloadImage;
33333 const art = await resolveArt(branch);
33334 if (art && art.artUrl && await load(art.artUrl)) {
33335 showReleaseCard({
33336 message: updateMessage(version, crossing ? art.name : ""),
33337 artUrl: art.artUrl,
33338 dismissKey,
33339 onUpdate: openUpdateScreen
33340 });
33341 return;
33342 }
33343 showToast({
33344 message: updateMessage(version, ""),
33345 persistent: true,
33346 dismissible: true,
33347 onDismiss: () => markNoticeDismissed(dismissKey),
33348 action: {
33349 label: __("Update now"),
33350 onClick: openUpdateScreen
33351 }
33352 });
33353 }
33354 function maybeShowNotices(deps2) {
33355 const { notices, openUrl, keyPrefix = "core-notice" } = deps2;
33356 if (!Array.isArray(notices)) {
33357 return;
33358 }
33359 for (const notice of notices) {
33360 if (!notice || typeof notice.id !== "string" || !notice.id || typeof notice.message !== "string" || !notice.message) {
33361 continue;
33362 }
33363 const dismissKey = `desktop-mode/${keyPrefix}:${notice.id}`;
33364 if (isNoticeDismissed(dismissKey)) {
33365 continue;
33366 }
33367 const label = notice.actionLabel;
33368 const actionUrl = notice.actionUrl;
33369 const windowTitle = notice.title || label || "";
33370 let action;
33371 if (label && actionUrl) {
33372 action = {
33373 label,
33374 onClick: () => openUrl({ url: actionUrl, title: windowTitle })
33375 };
33376 }
33377 showToast({
33378 message: notice.message,
33379 persistent: true,
33380 dismissible: true,
33381 onDismiss: () => markNoticeDismissed(dismissKey),
33382 action
33383 });
33384 }
33385 }
33386 const STARTER_WIDGET_ID = "desktop-mode/starter";
33387 const FILTER_NAMESPACE = "desktop-mode/dev-mode-gate";
33388 let _started = false;
33389 function setupDevModeWidgetGate({ osSettings, layer }) {
33390 if (_started) {
33391 return;
33392 }
33393 _started = true;
33394 addFilter(
33395 HOOKS.WIDGETS,
33396 FILTER_NAMESPACE,
33397 (defs) => {
33398 if (osSettings.getOsSettingsSnapshot().developerModeEnabled) {
33399 return defs;
33400 }
33401 return defs.filter((def) => def.id !== STARTER_WIDGET_ID);
33402 }
33403 );
33404 let developerModeEnabled = osSettings.getOsSettingsSnapshot().developerModeEnabled;
33405 osSettings.subscribeOsSettings((snapshot) => {
33406 if (snapshot.developerModeEnabled === developerModeEnabled) {
33407 return;
33408 }
33409 developerModeEnabled = snapshot.developerModeEnabled;
33410 if (developerModeEnabled) {
33411 layer.mountIfEnabled(STARTER_WIDGET_ID);
33412 } else {
33413 layer.unmount(STARTER_WIDGET_ID);
33414 }
33415 refreshWidgetPicker();
33416 });
33417 }
33418 function createWidgetRegistrySync(deps2) {
33419 const { layer } = deps2;
33420 const registered = /* @__PURE__ */ new Set();
33421 const loadedScripts = /* @__PURE__ */ new Set();
33422 const ensureScript = async (entry) => {
33423 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
33424 return;
33425 }
33426 try {
33427 await loadVendorScript(entry.scriptUrl, {
33428 translations: entry.scriptTranslations,
33429 l10n: entry.scriptL10n,
33430 before: entry.scriptBefore,
33431 after: entry.scriptAfter
33432 });
33433 } catch (err) {
33434 doAction(HOOKS.SHELL_ERROR, {
33435 scope: "widget-script-load",
33436 id: entry.id,
33437 error: err
33438 });
33439 return;
33440 }
33441 loadedScripts.add(entry.scriptUrl);
33442 };
33443 const buildDefFromEntry = (entry) => {
33444 const globals = window.desktopModeWidgets || {};
33445 const mount = globals[entry.id];
33446 if (!mount) {
33447 doAction(HOOKS.SHELL_ERROR, {
33448 scope: "widget-missing-mount",
33449 id: entry.id,
33450 error: new Error(
33451 `[desktop-mode] No mount callback on window.desktopModeWidgets["${entry.id}"]. Plugin script loaded but didn't register. Check the plugin's enqueue + global assignment.`
33452 )
33453 });
33454 return null;
33455 }
33456 return {
33457 id: entry.id,
33458 label: entry.label,
33459 description: entry.description,
33460 icon: entry.icon,
33461 movable: entry.movable,
33462 resizable: entry.resizable,
33463 minWidth: entry.minWidth || void 0,
33464 minHeight: entry.minHeight || void 0,
33465 maxWidth: entry.maxWidth || void 0,
33466 maxHeight: entry.maxHeight || void 0,
33467 defaultWidth: entry.defaultWidth || void 0,
33468 defaultHeight: entry.defaultHeight || void 0,
33469 mount
33470 };
33471 };
33472 const registerEntry = async (entry) => {
33473 if (registered.has(entry.id)) {
33474 return;
33475 }
33476 await ensureScript(entry);
33477 const def = buildDefFromEntry(entry);
33478 if (!def) {
33479 return;
33480 }
33481 try {
33482 register(def);
33483 } catch (err) {
33484 doAction(HOOKS.SHELL_ERROR, {
33485 scope: "widget-register",
33486 id: entry.id,
33487 error: err
33488 });
33489 return;
33490 }
33491 registered.add(entry.id);
33492 refreshWidgetPicker();
33493 if (layer) {
33494 layer.mountIfEnabled(entry.id);
33495 }
33496 };
33497 const unregisterEntry = (id) => {
33498 if (!registered.has(id)) {
33499 return;
33500 }
33501 layer?.unmount(id);
33502 unregister(id);
33503 registered.delete(id);
33504 refreshWidgetPicker();
33505 };
33506 return async (list2) => {
33507 const incoming = /* @__PURE__ */ new Set();
33508 for (const entry of list2) {
33509 incoming.add(entry.id);
33510 }
33511 for (const id of Array.from(registered)) {
33512 if (!incoming.has(id)) {
33513 unregisterEntry(id);
33514 }
33515 }
33516 for (const entry of list2) {
33517 if (!registered.has(entry.id)) {
33518 await registerEntry(entry);
33519 }
33520 }
33521 };
33522 }
33523 const WPD_COMPONENT_TAGS = [
33524 "wpd-section",
33525 "wpd-button",
33526 "wpd-swatch",
33527 "wpd-swatch-grid",
33528 "wpd-segmented",
33529 "wpd-segment",
33530 "wpd-select",
33531 "wpd-option",
33532 "wpd-multiselect",
33533 "wpd-color-field",
33534 "wpd-range-field",
33535 "wpd-text-field",
33536 "wpd-number-field",
33537 "wpd-checkbox",
33538 "wpd-checkbox-label",
33539 "wpd-toast",
33540 "wpd-toast-container",
33541 "wpd-tabs",
33542 "wpd-tab",
33543 "wpd-tabpanel",
33544 "wpd-window-button",
33545 "wpd-menu",
33546 "wpd-menu-item",
33547 "wpd-context-menu",
33548 "wpd-context-menu-option",
33549 "wpd-confirm-dialog",
33550 "wpd-modal",
33551 "wpd-user-search",
33552 "wpd-role-picker",
33553 "wpd-flyout",
33554 "wpd-tab-chip",
33555 "wpd-stack",
33556 "wpd-cluster",
33557 "wpd-icon",
33558 "wpd-body",
33559 "wpd-panel",
33560 "wpd-row",
33561 "wpd-grid",
33562 "wpd-display",
33563 "wpd-empty-state",
33564 "wpd-key",
33565 "wpd-code",
33566 "wpd-badge",
33567 "wpd-ribbon",
33568 "wpd-tile",
33569 "wpd-log",
33570 "wpd-steps",
33571 "wpd-step",
33572 "wpd-table",
33573 "wpd-spinner",
33574 "wpd-relative-time",
33575 "wpd-avatar",
33576 "wpd-textarea",
33577 "wpd-chip",
33578 "wpd-tag-input",
33579 "wpd-form",
33580 "wpd-save-status",
33581 "wpd-category-picker",
33582 "wpd-crumb-chain",
33583 "wpd-card",
33584 "wpd-rating-summary",
33585 "wpd-notice",
33586 "wpd-progress-bar"
33587 ];
33588 const KNOWN = new Set(WPD_COMPONENT_TAGS);
33589 const WARN_GRACE_MS = 2e3;
33590 const warnedTags = /* @__PURE__ */ new Set();
33591 const observedRoots = /* @__PURE__ */ new WeakSet();
33592 let started$2 = false;
33593 function distance(a, b) {
33594 const m = a.length;
33595 const n = b.length;
33596 if (m === 0) {
33597 return n;
33598 }
33599 if (n === 0) {
33600 return m;
33601 }
33602 const dp = new Array(n + 1);
33603 for (let j = 0; j <= n; j++) {
33604 dp[j] = j;
33605 }
33606 for (let i = 1; i <= m; i++) {
33607 let prev = dp[0];
33608 dp[0] = i;
33609 for (let j = 1; j <= n; j++) {
33610 const tmp = dp[j];
33611 dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
33612 prev = tmp;
33613 }
33614 }
33615 return dp[n];
33616 }
33617 function suggest(tag) {
33618 let best = null;
33619 let bestD = Infinity;
33620 for (const known of KNOWN) {
33621 const d = distance(tag, known);
33622 if (d < bestD) {
33623 bestD = d;
33624 best = known;
33625 }
33626 }
33627 return bestD > 0 && bestD <= 3 ? best : null;
33628 }
33629 function folderFor(tag) {
33630 return tag.startsWith("wpd-") ? tag.slice(4) : tag;
33631 }
33632 function warnFor(tag, sample) {
33633 if (warnedTags.has(tag)) {
33634 return;
33635 }
33636 warnedTags.add(tag);
33637 const isKnown = KNOWN.has(tag);
33638 if (isKnown) {
33639 const folder = folderFor(tag);
33640 console.error(
33641 `[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.
33642
33643 Fix — side-effect-import the component module from wherever you render it:
33644
33645 import '<rel>/ui/components/${folder}/${folder}';
33646
33647 Or pull every wpd-* component in one go (heavier — only do this from an entry bundle):
33648
33649 import '<rel>/ui/components';
33650
33651 See docs/components-reference.md for the full list.`,
33652 "\nFirst offending element:",
33653 sample
33654 );
33655 return;
33656 }
33657 const guess = suggest(tag);
33658 if (guess) {
33659 console.error(
33660 `[wp.desktop] <${tag}> is not a registered wpd-* component. Did you mean <${guess}>?
33661
33662 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'.`,
33663 "\nFirst offending element:",
33664 sample
33665 );
33666 return;
33667 }
33668 console.error(
33669 `[wp.desktop] <${tag}> looks like a wpd-* tag but no component by that name exists.
33670
33671 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.`,
33672 "\nFirst offending element:",
33673 sample
33674 );
33675 }
33676 function checkElement(el) {
33677 const tag = el.tagName.toLowerCase();
33678 if (!tag.startsWith("wpd-")) {
33679 return;
33680 }
33681 if (warnedTags.has(tag)) {
33682 return;
33683 }
33684 if (customElements.get(tag)) {
33685 return;
33686 }
33687 let settled = false;
33688 customElements.whenDefined(tag).then(() => {
33689 settled = true;
33690 });
33691 setTimeout(() => {
33692 if (settled) {
33693 return;
33694 }
33695 if (customElements.get(tag)) {
33696 return;
33697 }
33698 warnFor(tag, el);
33699 }, WARN_GRACE_MS);
33700 }
33701 function walk(root) {
33702 if (root instanceof Element) {
33703 checkElement(root);
33704 if (root.shadowRoot) {
33705 observeRoot(root.shadowRoot);
33706 }
33707 }
33708 const all2 = root.querySelectorAll("*");
33709 for (let i = 0; i < all2.length; i++) {
33710 const el = all2[i];
33711 checkElement(el);
33712 if (el.shadowRoot) {
33713 observeRoot(el.shadowRoot);
33714 }
33715 }
33716 }
33717 function observeRoot(root) {
33718 if (observedRoots.has(root)) {
33719 return;
33720 }
33721 observedRoots.add(root);
33722 walk(root);
33723 const mo = new MutationObserver((records) => {
33724 for (let i = 0; i < records.length; i++) {
33725 const added = records[i].addedNodes;
33726 for (let j = 0; j < added.length; j++) {
33727 const node = added[j];
33728 if (node.nodeType === 1) {
33729 walk(node);
33730 }
33731 }
33732 }
33733 });
33734 mo.observe(root, { childList: true, subtree: true });
33735 }
33736 function patchAttachShadow() {
33737 const proto = Element.prototype;
33738 const original = proto.attachShadow;
33739 if (original.__wpdPatched) {
33740 return;
33741 }
33742 const patched = function(init2) {
33743 const root = original.call(this, init2);
33744 if (root.mode === "open") {
33745 observeRoot(root);
33746 }
33747 return root;
33748 };
33749 patched.__wpdPatched = true;
33750 proto.attachShadow = patched;
33751 }
33752 function startMissingImportWarner() {
33753 if (started$2) {
33754 return;
33755 }
33756 if (typeof document === "undefined") {
33757 return;
33758 }
33759 started$2 = true;
33760 patchAttachShadow();
33761 observeRoot(document);
33762 }
33763 const TRASHABLE_SHORTCUT_KINDS = /* @__PURE__ */ new Set(["post"]);
33764 function getMyWordpressTrashApi() {
33765 const api = window.wp?.desktop?.myWordpress;
33766 return api && typeof api.trashEntity === "function" ? api : null;
33767 }
33768 const TRASH_DROP_ACTIVE_ATTR = "data-desktop-mode-trash-drop-active";
33769 const RECYCLE_BIN_WINDOW_ID = "desktop-mode-recycle-bin";
33770 const BIN_TILE_SELECTORS = [
33771 `.desktop-mode-file-tile[data-file-ref="${RECYCLE_BIN_WINDOW_ID}"]`,
33772 `[data-icon-id="${RECYCLE_BIN_WINDOW_ID}"]`,
33773 `[data-system-id="${RECYCLE_BIN_WINDOW_ID}"]`
33774 ];
33775 function findBinTile() {
33776 for (const sel of BIN_TILE_SELECTORS) {
33777 const el = document.querySelector(sel);
33778 if (el instanceof HTMLElement) {
33779 return el;
33780 }
33781 }
33782 return null;
33783 }
33784 let _installed = false;
33785 let _dockDeregister = null;
33786 let _windowDeregister = null;
33787 let _binMutationObserver = null;
33788 function isDesktopFilePayload(session) {
33789 return session.payload.type === "desktop-file";
33790 }
33791 function isShortcutPayload(session) {
33792 return session.payload.type === "shortcut";
33793 }
33794 function isTrashableShortcut(data) {
33795 if (!data.kind || !data.ref || !data.entityId) {
33796 return false;
33797 }
33798 if (!TRASHABLE_SHORTCUT_KINDS.has(data.kind)) {
33799 return false;
33800 }
33801 const numericRef = Number.parseInt(data.ref, 10);
33802 if (!Number.isFinite(numericRef) || numericRef <= 0) {
33803 return false;
33804 }
33805 return getMyWordpressTrashApi() !== null;
33806 }
33807 function registerOn(dragManager, id, el) {
33808 return dragManager.registerDropTarget({
33809 id,
33810 element: el,
33811 // Override the ghost-chip label: while the cursor is over
33812 // the bin the user is trashing, not creating a shortcut /
33813 // moving the placement. The DragManager swaps this in for
33814 // the payload-default "Drop here to create shortcut" /
33815 // "Drop here to move" chip text whenever this target is the
33816 // current accept-mode target.
33817 acceptLabel: __("Move to Trash", "desktop-mode"),
33818 // Reject the drop UP FRONT when the viewer can't trash the
33819 // payload's placement (e.g. an item inside a read-only
33820 // shared folder, or someone else's tile in a shared
33821 // namespace). `accept` flipping to `false` means the
33822 // drop-active highlight never lights up + onDrop never
33823 // fires + the drag manager surfaces a `rejected` outcome.
33824 // The user sees the icon snap back instead of attempting a
33825 // REST call that would 403 and only log to the console.
33826 accept: (payload) => {
33827 if (payload.type === "desktop-file") {
33828 const data = payload.data;
33829 const placement = data?.placement;
33830 if (!placement) {
33831 return false;
33832 }
33833 if (placement.file?.ref === RECYCLE_BIN_WINDOW_ID) {
33834 return false;
33835 }
33836 return placement.canTrash !== false;
33837 }
33838 if (payload.type === "shortcut") {
33839 const data = payload.data;
33840 return isTrashableShortcut(data);
33841 }
33842 return recycleBinPayloadAccepts(payload);
33843 },
33844 onEnter: () => {
33845 el.setAttribute(TRASH_DROP_ACTIVE_ATTR, "");
33846 },
33847 onLeave: () => {
33848 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
33849 },
33850 onDrop: (session, ev) => {
33851 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
33852 if (isDesktopFilePayload(session)) {
33853 const placement = session.payload.data.placement;
33854 void trashByFileType(placement);
33855 return;
33856 }
33857 if (session.payload.type !== "shortcut" && recycleBinPayloadDrop(session, ev)) {
33858 return;
33859 }
33860 if (isShortcutPayload(session)) {
33861 const data = session.payload.data;
33862 const api = getMyWordpressTrashApi();
33863 if (!api?.trashEntity || !data.entityId) {
33864 return;
33865 }
33866 const numericRef = Number.parseInt(data.ref, 10);
33867 if (!Number.isFinite(numericRef) || numericRef <= 0) {
33868 return;
33869 }
33870 void api.trashEntity(data.entityId, numericRef).catch(
33871 (err) => {
33872 console.error(
33873 "[desktop-mode] recycle-bin: shortcut trash failed:",
33874 err
33875 );
33876 }
33877 );
33878 }
33879 }
33880 });
33881 }
33882 function installRecycleBinDropTargets(dragManager) {
33883 if (_installed) {
33884 return;
33885 }
33886 _installed = true;
33887 const reprobeTile = () => {
33888 const el = findBinTile();
33889 if (!el) {
33890 _dockDeregister?.();
33891 _dockDeregister = null;
33892 return;
33893 }
33894 if (_dockDeregister && getRegisteredElementId(dragManager) === el) {
33895 return;
33896 }
33897 _dockDeregister?.();
33898 _dockDeregister = registerOn(dragManager, "recycle-bin-dock", el);
33899 };
33900 reprobeTile();
33901 document.addEventListener("desktop-mode-files-changed", reprobeTile);
33902 addAction(
33903 HOOKS.DESKTOP_ICONS_RENDERED,
33904 "desktop-mode/files/recycle-bin-icons-target",
33905 reprobeTile
33906 );
33907 addAction(
33908 HOOKS.DOCK_AFTER_RENDER,
33909 "desktop-mode/files/recycle-bin-dock-target",
33910 reprobeTile
33911 );
33912 if (typeof MutationObserver !== "undefined") {
33913 _binMutationObserver = new MutationObserver(() => {
33914 reprobeTile();
33915 });
33916 const desktopArea = document.getElementById("desktop-mode-area") ?? document.body;
33917 _binMutationObserver.observe(desktopArea, {
33918 childList: true,
33919 subtree: true
33920 });
33921 }
33922 addAction(
33923 HOOKS.WINDOW_OPENED,
33924 "desktop-mode/files/recycle-bin-window-target",
33925 (detail) => {
33926 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
33927 return;
33928 }
33929 _windowDeregister?.();
33930 _windowDeregister = null;
33931 const el = document.querySelector(
33932 "[data-desktop-mode-recycle-bin-root]"
33933 );
33934 if (el instanceof HTMLElement) {
33935 _windowDeregister = registerOn(
33936 dragManager,
33937 "recycle-bin-window",
33938 el
33939 );
33940 }
33941 }
33942 );
33943 addAction(
33944 HOOKS.WINDOW_CLOSED,
33945 "desktop-mode/files/recycle-bin-window-cleanup",
33946 (detail) => {
33947 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
33948 return;
33949 }
33950 _windowDeregister?.();
33951 _windowDeregister = null;
33952 }
33953 );
33954 }
33955 function getRegisteredElementId(dragManager) {
33956 const t = dragManager.debug().listTargets().find((target2) => target2.id === "recycle-bin-dock");
33957 return t ? t.element : null;
33958 }
33959 let started$1 = false;
33960 let highWaterMs = 0;
33961 function startFilesHeartbeat() {
33962 if (started$1) {
33963 return;
33964 }
33965 started$1 = true;
33966 heartbeat.contribute("desktop_mode_files_subscribe", () => {
33967 const state2 = getFilesState();
33968 const folderVersions = {};
33969 for (const [id, folder] of state2.folders) {
33970 folderVersions[String(id)] = folder.updatedAtMs;
33971 }
33972 return {
33973 folderVersions,
33974 placementsVersion: highWaterMs,
33975 sharesVersion: sharesStore().state.sharesVersion
33976 };
33977 });
33978 heartbeat.subscribe("desktop_mode_files", (payload) => {
33979 applyDelta(payload);
33980 });
33981 }
33982 function applyDelta(payload) {
33983 const folders = payload.folders ?? [];
33984 for (const folder of folders) {
33985 upsertFolder(folder, "remote");
33986 if (folder.updatedAtMs > highWaterMs) {
33987 highWaterMs = folder.updatedAtMs;
33988 }
33989 }
33990 const placements = payload.placements ?? [];
33991 for (const placement of placements) {
33992 upsertPlacement(placement, "remote");
33993 if (placement.updatedAtMs > highWaterMs) {
33994 highWaterMs = placement.updatedAtMs;
33995 }
33996 }
33997 const removed = payload.removed ?? {};
33998 for (const id of removed.folders ?? []) {
33999 removeFolder(id, "remote");
34000 }
34001 for (const id of removed.placements ?? []) {
34002 removePlacement(id, "remote");
34003 }
34004 if (typeof payload.serverTimeMs === "number" && payload.serverTimeMs > highWaterMs) {
34005 highWaterMs = payload.serverTimeMs;
34006 }
34007 const pending2 = payload.shares?.pending;
34008 if (Array.isArray(pending2) && pending2.length > 0) {
34009 ingestPendingInvites(pending2);
34010 }
34011 if (payload.truncated) {
34012 const hydrated = Array.from(getFilesState().hydratedFolders);
34013 for (const folderId of hydrated) {
34014 void listPlacements(folderId).then((res) => {
34015 setFolderPlacements(folderId, res.placements);
34016 }).catch(() => {
34017 });
34018 }
34019 }
34020 }
34021 let started = false;
34022 const unsubscribers = [];
34023 function startFilesRestoreSync() {
34024 if (started) {
34025 return;
34026 }
34027 started = true;
34028 const onChange = (payload) => {
34029 const detail = payload;
34030 if (!detail || detail.action !== "untrashed") {
34031 return;
34032 }
34033 resyncFromServer();
34034 };
34035 unsubscribers.push(
34036 subscribe$2("desktop-mode.placement.changed", onChange),
34037 subscribe$2("desktop-mode.shortcut.changed", onChange),
34038 subscribe$2("desktop-mode.folder.changed", onChange)
34039 );
34040 }
34041 function resyncFromServer() {
34042 void listFolders().then((res) => {
34043 setFolders(res.folders);
34044 }).catch((err) => {
34045 console.error(
34046 "[desktop-mode] files restore-sync: listFolders failed",
34047 err
34048 );
34049 });
34050 const hydrated = Array.from(getFilesState().hydratedFolders);
34051 for (const folderId of hydrated) {
34052 void listPlacements(folderId).then((res) => {
34053 setFolderPlacements(folderId, res.placements);
34054 }).catch((err) => {
34055 console.error(
34056 "[desktop-mode] files restore-sync: listPlacements failed for",
34057 folderId,
34058 err
34059 );
34060 });
34061 }
34062 }
34063 const MENU_CLASS = "desktop-mode-wallpaper-menu";
34064 let activeMenu = null;
34065 function isWallpaperMenuOpen() {
34066 return activeMenu !== null;
34067 }
34068 let openGeneration = 0;
34069 function openWallpaperMenu(host, pos, items, options = {}) {
34070 closeWallpaperMenu();
34071 const myGen = ++openGeneration;
34072 openWithShellOverlays(
34073 () => myGen === openGeneration,
34074 () => openWallpaperMenuImmediate(host, pos, items, options)
34075 );
34076 }
34077 function openWallpaperMenuImmediate(host, pos, items, options = {}) {
34078 if (items.length === 0) {
34079 return;
34080 }
34081 items = items.slice().sort((a, b) => {
34082 const sa = typeof a.sort === "number" ? a.sort : 100;
34083 const sb = typeof b.sort === "number" ? b.sort : 100;
34084 if (sa !== sb) {
34085 return sa - sb;
34086 }
34087 return a.label.localeCompare(b.label);
34088 });
34089 const menu = document.createElement("wpd-context-menu");
34090 menu.setAttribute("open", "");
34091 menu.classList.add(MENU_CLASS);
34092 menu.style.left = `${pos.x}px`;
34093 menu.style.top = `${pos.y}px`;
34094 const itemById = /* @__PURE__ */ new Map();
34095 let activeFlyout2 = null;
34096 let activeFlyoutParent = null;
34097 const closeActiveFlyout = () => {
34098 if (activeFlyout2) {
34099 activeFlyout2.remove();
34100 activeFlyout2 = null;
34101 activeFlyoutParent = null;
34102 }
34103 };
34104 for (const item of items) {
34105 itemById.set(item.id, item);
34106 const opt = document.createElement("wpd-context-menu-option");
34107 opt.dataset.menuItemId = item.id;
34108 opt.setAttribute("value", item.id);
34109 if (item.heading) {
34110 opt.setAttribute("heading", "");
34111 }
34112 if (item.disabled) {
34113 opt.setAttribute("disabled", "");
34114 }
34115 if (item.icon) {
34116 opt.setAttribute("icon", sanitizeClass(item.icon));
34117 }
34118 const hasChildren2 = Array.isArray(item.children) && item.children.length > 0;
34119 if (hasChildren2) {
34120 opt.setAttribute("has-children", "");
34121 }
34122 opt.textContent = item.label;
34123 opt.addEventListener("mouseenter", () => {
34124 if (hasChildren2) {
34125 openFlyout2(item, opt);
34126 return;
34127 }
34128 closeActiveFlyout();
34129 });
34130 menu.appendChild(opt);
34131 }
34132 menu.addEventListener("wpd-context-menu-pick", (e) => {
34133 const detail = e.detail;
34134 const item = itemById.get(detail.id) ?? null;
34135 if (!item) {
34136 return;
34137 }
34138 if (Array.isArray(item.children) && item.children.length > 0) {
34139 e.stopPropagation();
34140 if (activeFlyoutParent && activeFlyoutParent.id === item.id) {
34141 closeActiveFlyout();
34142 return;
34143 }
34144 const anchor = menu.querySelector(
34145 `[data-menu-item-id="${item.id}"]`
34146 );
34147 if (anchor) {
34148 openFlyout2(item, anchor);
34149 }
34150 return;
34151 }
34152 closeWallpaperMenu();
34153 void item.onClick(new MouseEvent("click"));
34154 });
34155 function openFlyout2(parent, anchor) {
34156 closeActiveFlyout();
34157 const fly = document.createElement("wpd-context-menu");
34158 fly.setAttribute("open", "");
34159 fly.classList.add(MENU_CLASS, `${MENU_CLASS}--flyout`);
34160 fly.dataset.parentId = parent.id;
34161 const sortedKids = (parent.children ?? []).slice().sort((a, b) => {
34162 const sa = typeof a.sort === "number" ? a.sort : 100;
34163 const sb = typeof b.sort === "number" ? b.sort : 100;
34164 if (sa !== sb) {
34165 return sa - sb;
34166 }
34167 return a.label.localeCompare(b.label);
34168 });
34169 for (const child of sortedKids) {
34170 const kopt = document.createElement("wpd-context-menu-option");
34171 kopt.dataset.menuItemId = child.id;
34172 kopt.setAttribute("value", child.id);
34173 if (child.icon) {
34174 kopt.setAttribute("icon", sanitizeClass(child.icon));
34175 }
34176 if (child.disabled) {
34177 kopt.setAttribute("disabled", "");
34178 }
34179 if (child.checked) {
34180 kopt.setAttribute("checked", "");
34181 }
34182 kopt.textContent = child.label;
34183 kopt.addEventListener("wpd-context-menu-pick", (e) => {
34184 e.stopPropagation();
34185 closeWallpaperMenu();
34186 void child.onClick(new MouseEvent("click"));
34187 });
34188 fly.appendChild(kopt);
34189 }
34190 document.body.appendChild(fly);
34191 activeFlyout2 = fly;
34192 activeFlyoutParent = parent;
34193 positionFlyout2(fly, anchor);
34194 }
34195 function positionFlyout2(fly, anchor) {
34196 const ar = anchor.getBoundingClientRect();
34197 fly.style.position = "fixed";
34198 fly.style.left = `${ar.right}px`;
34199 fly.style.top = `${ar.top}px`;
34200 const fr = fly.getBoundingClientRect();
34201 if (fr.right > window.innerWidth) {
34202 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
34203 }
34204 if (fr.bottom > window.innerHeight) {
34205 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
34206 }
34207 }
34208 host.appendChild(menu);
34209 activeMenu = menu;
34210 const rect = menu.getBoundingClientRect();
34211 if (rect.right > window.innerWidth) {
34212 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
34213 }
34214 if (rect.bottom > window.innerHeight) {
34215 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
34216 }
34217 const detach = attachDismissable(menu, {
34218 close: () => closeWallpaperMenu(),
34219 siblingSelectors: [`.${MENU_CLASS}--flyout`],
34220 excludeOutsideTarget: options.excludeOutsideTarget
34221 });
34222 menu.addEventListener("wallpaper-menu-closed", detach);
34223 doAction("desktop-mode.wallpaper-menu.opened", { items: items.map((i) => i.id) });
34224 }
34225 function closeWallpaperMenu() {
34226 if (!activeMenu) {
34227 return;
34228 }
34229 document.querySelectorAll(`.${MENU_CLASS}--flyout`).forEach((el) => el.remove());
34230 activeMenu.dispatchEvent(new CustomEvent("wallpaper-menu-closed"));
34231 activeMenu.remove();
34232 activeMenu = null;
34233 doAction("desktop-mode.wallpaper-menu.closed", {});
34234 }
34235 function buildMenuItems(deps2) {
34236 const builtIn = [
34237 {
34238 id: "create-folder",
34239 label: deps2.labels.createFolder,
34240 icon: "dashicons-portfolio",
34241 sort: 10,
34242 onClick: () => deps2.createFolder()
34243 },
34244 {
34245 id: "new-url",
34246 label: deps2.labels.newUrl,
34247 icon: "dashicons-admin-links",
34248 sort: 12,
34249 onClick: () => deps2.createUrl()
34250 },
34251 {
34252 id: "sort-by",
34253 label: deps2.labels.sortHeading,
34254 icon: "dashicons-sort",
34255 sort: 16,
34256 onClick: () => void 0,
34257 children: [
34258 {
34259 id: "sort-name-asc",
34260 label: deps2.labels.sortNameAsc,
34261 sort: 10,
34262 checked: deps2.currentSortMode === "name-asc",
34263 onClick: () => deps2.sortIcons("name-asc")
34264 },
34265 {
34266 id: "sort-name-desc",
34267 label: deps2.labels.sortNameDesc,
34268 sort: 20,
34269 checked: deps2.currentSortMode === "name-desc",
34270 onClick: () => deps2.sortIcons("name-desc")
34271 },
34272 {
34273 id: "sort-date-desc",
34274 label: deps2.labels.sortDateDesc,
34275 sort: 30,
34276 checked: deps2.currentSortMode === "date-desc",
34277 onClick: () => deps2.sortIcons("date-desc")
34278 },
34279 {
34280 id: "sort-date-asc",
34281 label: deps2.labels.sortDateAsc,
34282 sort: 40,
34283 checked: deps2.currentSortMode === "date-asc",
34284 onClick: () => deps2.sortIcons("date-asc")
34285 }
34286 ]
34287 },
34288 ...deps2.includeShowDesktop === false ? [] : [
34289 {
34290 id: "show-desktop",
34291 label: deps2.labels.showDesktop,
34292 icon: "dashicons-desktop",
34293 sort: 20,
34294 onClick: () => deps2.toggleShowDesktop()
34295 }
34296 ],
34297 {
34298 id: "os-settings",
34299 label: deps2.labels.osSettings,
34300 icon: "dashicons-admin-generic",
34301 sort: 30,
34302 onClick: () => deps2.openOsSettings()
34303 }
34304 ];
34305 const serverItems = (deps2.serverItems ?? []).map(
34306 (s) => serverItemToMenuItem(s, deps2)
34307 );
34308 const merged = [...builtIn, ...serverItems];
34309 const filtered = applyFilters(
34310 "desktop-mode.wallpaper-context-menu",
34311 merged
34312 );
34313 return Array.isArray(filtered) ? filtered : merged;
34314 }
34315 function serverItemToMenuItem(server, deps2) {
34316 return {
34317 id: server.id,
34318 label: server.label,
34319 icon: server.icon,
34320 sort: server.sort,
34321 disabled: server.disabled,
34322 onClick: () => {
34323 if (server.callbackId) {
34324 const cb = deps2.serverCallbacks?.[server.callbackId];
34325 if (typeof cb === "function") {
34326 return cb();
34327 }
34328 }
34329 doAction("desktop-mode.wallpaper-context-menu.activated", {
34330 id: server.id,
34331 callbackId: server.callbackId ?? ""
34332 });
34333 }
34334 };
34335 }
34336 function sanitizeClass(raw) {
34337 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
34338 }
34339 const ROOT_CLASS = "desktop-mode-url-dialog";
34340 let active = null;
34341 function closeUrlDialog() {
34342 if (!active) {
34343 return;
34344 }
34345 active.dispatchEvent(new CustomEvent("url-dialog-closed"));
34346 active.remove();
34347 active = null;
34348 doAction("desktop-mode.files.url-dialog.closed", {});
34349 }
34350 function openUrlDialog(options) {
34351 closeUrlDialog();
34352 const decision = applyFilters(
34353 "desktop-mode.files.url-dialog",
34354 null,
34355 options
34356 );
34357 if (decision === false) {
34358 return;
34359 }
34360 const overlay = document.createElement("div");
34361 overlay.className = `${ROOT_CLASS}__overlay desktop-mode-create-folder-dialog__overlay`;
34362 overlay.setAttribute("role", "presentation");
34363 const dialog2 = document.createElement("div");
34364 dialog2.className = `${ROOT_CLASS} desktop-mode-create-folder-dialog`;
34365 dialog2.setAttribute("role", "dialog");
34366 dialog2.setAttribute("aria-modal", "true");
34367 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS}-title`);
34368 const title = document.createElement("h2");
34369 title.id = `${ROOT_CLASS}-title`;
34370 title.className = "desktop-mode-create-folder-dialog__title";
34371 title.textContent = options.title;
34372 dialog2.appendChild(title);
34373 if (options.description) {
34374 const desc = document.createElement("p");
34375 desc.className = `${ROOT_CLASS}__description`;
34376 desc.textContent = options.description;
34377 dialog2.appendChild(desc);
34378 }
34379 const nameField = document.createElement("wpd-text-field");
34380 nameField.setAttribute("label", options.nameLabel ?? "Name");
34381 nameField.setAttribute("value", options.initialName ?? "");
34382 nameField.setAttribute("placeholder", "My web app");
34383 nameField.setAttribute("autocomplete", "off");
34384 dialog2.appendChild(nameField);
34385 const urlField = document.createElement("wpd-text-field");
34386 urlField.setAttribute("label", options.urlLabel ?? "URL");
34387 urlField.setAttribute("value", options.initialUrl ?? "https://");
34388 urlField.setAttribute("placeholder", "https://example.com");
34389 urlField.setAttribute("type", "url");
34390 urlField.setAttribute("autocomplete", "off");
34391 dialog2.appendChild(urlField);
34392 const error = document.createElement("p");
34393 error.className = "desktop-mode-create-folder-dialog__error";
34394 error.hidden = true;
34395 error.setAttribute("role", "alert");
34396 dialog2.appendChild(error);
34397 const actions = document.createElement("div");
34398 actions.className = "desktop-mode-create-folder-dialog__actions";
34399 const cancel = document.createElement("button");
34400 cancel.type = "button";
34401 cancel.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--secondary";
34402 cancel.textContent = "Cancel";
34403 const submit = document.createElement("button");
34404 submit.type = "button";
34405 submit.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--primary";
34406 submit.textContent = options.submitLabel ?? "Create";
34407 actions.appendChild(cancel);
34408 actions.appendChild(submit);
34409 dialog2.appendChild(actions);
34410 overlay.appendChild(dialog2);
34411 document.body.appendChild(overlay);
34412 active = overlay;
34413 queueMicrotask(() => {
34414 const input = nameField.shadowRoot?.querySelector("input");
34415 input?.focus();
34416 input?.select();
34417 });
34418 doAction("desktop-mode.files.url-dialog.opened", {});
34419 const readValue = (field) => {
34420 const v = field.value;
34421 if (typeof v === "string") {
34422 return v;
34423 }
34424 return field.shadowRoot?.querySelector("input")?.value ?? "";
34425 };
34426 const setBusy = (busy) => {
34427 nameField.disabled = busy;
34428 urlField.disabled = busy;
34429 cancel.disabled = busy;
34430 submit.disabled = busy;
34431 dialog2.classList.toggle("desktop-mode-create-folder-dialog--busy", busy);
34432 };
34433 const showError = (msg) => {
34434 error.textContent = msg;
34435 error.hidden = false;
34436 };
34437 const doCancel = () => {
34438 closeUrlDialog();
34439 options.onCancel?.();
34440 };
34441 const doSubmit = async () => {
34442 const url = readValue(urlField).trim();
34443 if (!url) {
34444 showError("Please enter a URL.");
34445 return;
34446 }
34447 const finalUrl = /^[a-z][a-z0-9+\-.]*:/i.test(url) ? url : `https://${url}`;
34448 try {
34449 new URL(finalUrl);
34450 } catch {
34451 showError("That doesn't look like a valid URL.");
34452 return;
34453 }
34454 const name = readValue(nameField).trim();
34455 error.hidden = true;
34456 setBusy(true);
34457 try {
34458 await options.onSubmit({ name, url: finalUrl });
34459 closeUrlDialog();
34460 } catch (err) {
34461 setBusy(false);
34462 showError(err instanceof Error ? err.message : "Could not save.");
34463 }
34464 };
34465 cancel.addEventListener("click", () => doCancel());
34466 submit.addEventListener("click", () => void doSubmit());
34467 overlay.addEventListener("click", (e) => {
34468 if (e.target === overlay) {
34469 doCancel();
34470 }
34471 });
34472 const onKey = (e) => {
34473 if (e.key === "Escape") {
34474 e.preventDefault();
34475 doCancel();
34476 } else if (e.key === "Enter" && !e.isComposing) {
34477 e.preventDefault();
34478 void doSubmit();
34479 }
34480 };
34481 dialog2.addEventListener("keydown", onKey);
34482 overlay.addEventListener("url-dialog-closed", () => {
34483 dialog2.removeEventListener("keydown", onKey);
34484 });
34485 }
34486 const _earlyReadyQueue = [];
34487 let _earlyReady = false;
34488 (function installEarlyDesktopShim() {
34489 const w = window;
34490 if (!w.wp) {
34491 w.wp = {};
34492 }
34493 if (w.wp.desktop) {
34494 return;
34495 }
34496 const shim = {
34497 whenReady(cb) {
34498 if (typeof cb !== "function") {
34499 return;
34500 }
34501 if (_earlyReady) {
34502 Promise.resolve().then(cb);
34503 return;
34504 }
34505 _earlyReadyQueue.push(cb);
34506 },
34507 ready(cb) {
34508 shim.whenReady(cb);
34509 },
34510 isReady() {
34511 return _earlyReady;
34512 }
34513 };
34514 w.wp.desktop = shim;
34515 })();
34516 const OS_SETTINGS_WINDOW_ID = "desktop-mode-os-settings";
34517 let _idleBootQueue = [];
34518 let _idleBootTimeout = Number.POSITIVE_INFINITY;
34519 let _idleBootScheduled = false;
34520 function scheduleIdleBoot(cb, timeout = 1500) {
34521 _idleBootQueue.push(cb);
34522 if (timeout < _idleBootTimeout) {
34523 _idleBootTimeout = timeout;
34524 }
34525 if (_idleBootScheduled) {
34526 return;
34527 }
34528 _idleBootScheduled = true;
34529 const drain = () => {
34530 const callbacks = _idleBootQueue;
34531 _idleBootQueue = [];
34532 _idleBootTimeout = Number.POSITIVE_INFINITY;
34533 _idleBootScheduled = false;
34534 for (const fn of callbacks) {
34535 try {
34536 fn();
34537 } catch (err) {
34538 if (typeof console !== "undefined") {
34539 console.error(
34540 "[desktop-mode] scheduleIdleBoot callback threw:",
34541 err
34542 );
34543 }
34544 }
34545 }
34546 };
34547 if (typeof window.requestIdleCallback === "function") {
34548 window.requestIdleCallback(drain, { timeout: _idleBootTimeout });
34549 } else {
34550 window.setTimeout(drain, 0);
34551 }
34552 }
34553 function init() {
34554 const config = window.desktopModeConfig;
34555 if (!config) {
34556 return;
34557 }
34558 const desktopArea = document.getElementById("desktop-mode-area");
34559 if (!desktopArea) {
34560 return;
34561 }
34562 const manager2 = new WindowManager(desktopArea);
34563 const wallpaperEl = document.getElementById("desktop-mode-wallpaper");
34564 const pluginUrl = config.pluginUrl || "";
34565 let wallpaperLayer = null;
34566 if (wallpaperEl) {
34567 wallpaperLayer = new WallpaperLayer(wallpaperEl, pluginUrl);
34568 }
34569 const widgetsEl = document.getElementById("desktop-mode-widgets");
34570 let widgetLayer = null;
34571 registerBuiltInWidgets();
34572 installDefaultDockRailRenderer();
34573 if (widgetsEl) {
34574 widgetLayer = new WidgetLayer(widgetsEl, pluginUrl);
34575 }
34576 registerModule({
34577 id: "pixijs",
34578 url: `${pluginUrl}/assets/vendor/pixi.min.js`,
34579 isReady: () => typeof window.PIXI !== "undefined"
34580 });
34581 const osSettings = new OsSettings(
34582 {
34583 mediaUrl: config.mediaUrl,
34584 restNonce: config.restNonce,
34585 canUpload: !!config.canUpload,
34586 isAdmin: !!config.currentUserIsAdmin,
34587 extendedOptions: config.extendedOptions ?? null,
34588 extendedOptionsUrl: config.extendedOptionsUrl ?? "",
34589 osSettingsPanelBundleUrl: config.osSettingsPanelBundleUrl ?? ""
34590 },
34591 wallpaperLayer ?? new WallpaperLayer(document.createElement("div"), pluginUrl)
34592 );
34593 osSettings.apply();
34594 if (widgetLayer) {
34595 setupDevModeWidgetGate({ osSettings, layer: widgetLayer });
34596 }
34597 const aiAssistant = new AiAssistantStub(
34598 {
34599 aiSearchUrl: config.aiSearchUrl ?? "",
34600 aiSearchStreamUrl: config.aiSearchStreamUrl ?? "",
34601 restNonce: config.restNonce,
34602 // Progress streaming is on by default now that the per-user
34603 // transport picker is gone; the assistant falls back gracefully
34604 // if the host drops the SSE connection.
34605 getTransport: () => "sse",
34606 // AI mode is usable when the APIs are present and a provider is
34607 // configured; the Commands palette works regardless. Read live so
34608 // connecting a provider or flipping the "AI assistant" toggle takes
34609 // effect on the next open — no reload.
34610 isAiAvailable: () => config.aiAssistant?.available === true && config.aiAssistant?.assistantProviderConfigured === true,
34611 isOverrideEnabled: () => osSettings.getOsSettingsSnapshot().ai.enabled !== false
34612 },
34613 config.aiAssistantBundleUrl ?? ""
34614 );
34615 aiAssistant.attachAsk(
34616 createAsk({
34617 config: () => config,
34618 fallbackContext: () => ({
34619 close: () => aiAssistant.close(),
34620 openInWindow: (url, title, icon) => {
34621 manager2.open({
34622 url,
34623 title,
34624 icon: icon ?? "dashicons-admin-generic"
34625 });
34626 },
34627 confirm: (msg) => wpdConfirm({ message: msg })
34628 })
34629 })
34630 );
34631 const dragBridge = new DragBridge();
34632 const dragManager = new DragManager();
34633 document.addEventListener(DRAG_EVENTS.START, (e) => {
34634 const detail = e.detail;
34635 const payload = detail?.payload;
34636 if (!payload) {
34637 return;
34638 }
34639 if (payload.type !== "shortcut" && payload.type !== "desktop-file") {
34640 return;
34641 }
34642 const bridgePayload = payload.data?.bridgePayload;
34643 if (bridgePayload) {
34644 dragBridge.start(bridgePayload);
34645 }
34646 });
34647 document.addEventListener(DRAG_EVENTS.END, () => {
34648 dragBridge.end();
34649 });
34650 scheduleIdleBoot(() => installIframeDropTargets(dragManager));
34651 scheduleIdleBoot(() => installFocusWindowOnDragHover(manager2));
34652 window.addEventListener("message", (e) => {
34653 if (e.origin !== window.location.origin) {
34654 return;
34655 }
34656 const data = e.data;
34657 if (!data || data.type !== "desktop-mode-drop-failed") {
34658 return;
34659 }
34660 showToast({
34661 message: "Could not insert into the editor."
34662 });
34663 });
34664 registerPalette({
34665 id: "desktop-mode-ai-assistant",
34666 label: "AI Assistant",
34667 open: () => aiAssistant.open(),
34668 close: () => aiAssistant.close(),
34669 isOpen: () => aiAssistant.isOpen
34670 });
34671 installPaletteShortcut();
34672 installWindowSwitcherShortcut(manager2);
34673 installDesktopArrowShortcuts(manager2);
34674 scheduleIdleBoot(() => {
34675 new IframeCommandBridge({
34676 manager: manager2,
34677 adminUrl: config.adminUrl
34678 }).install();
34679 new ShellCommandHarvester({
34680 manager: manager2,
34681 adminUrl: config.adminUrl
34682 }).install();
34683 });
34684 document.addEventListener("desktop-mode-open-ai", () => {
34685 openPaletteOnly("desktop-mode-ai-assistant");
34686 });
34687 document.addEventListener(
34688 "click",
34689 (e) => {
34690 const target2 = e.target;
34691 if (!(target2 instanceof Element) || !target2.closest("#wp-admin-bar-command-palette")) {
34692 return;
34693 }
34694 e.preventDefault();
34695 e.stopImmediatePropagation();
34696 openPaletteOnly("desktop-mode-ai-assistant");
34697 },
34698 true
34699 );
34700 const bottomDockEl = document.getElementById("desktop-mode-dock");
34701 const shellEl = document.getElementById("desktop-mode-shell");
34702 const shellBody = shellEl?.querySelector(
34703 ".desktop-mode-shell__body"
34704 );
34705 let layoutDispatcher = null;
34706 const nativeWindows = createNativeWindowSync({
34707 manager: manager2,
34708 appendSystemTile: (item) => layoutDispatcher?.appendSystemTile(item),
34709 removeSystemTile: (id) => layoutDispatcher?.removeSystemTile(id)
34710 });
34711 const syncNativeWindows = nativeWindows.sync;
34712 bindNativeUrlRemap({
34713 getSnapshot: () => osSettings.getOsSettingsSnapshot(),
34714 openById: (id) => nativeWindows.openById(id),
34715 adminUrl: config.adminUrl
34716 });
34717 const findDockEntryForUrl2 = (url) => {
34718 const targetSlug = deriveWindowId(url, config.adminUrl);
34719 const items = layoutDispatcher ? layoutDispatcher.getMenuItems() : config.dockItems ?? [];
34720 for (const item of items) {
34721 if (deriveWindowId(item.url, config.adminUrl) === targetSlug) {
34722 return {
34723 title: item.title,
34724 icon: item.icon,
34725 url: item.url,
34726 submenu: item.submenu,
34727 multi: item.multi
34728 };
34729 }
34730 for (const sub of item.submenu ?? []) {
34731 if (deriveWindowId(sub.url, config.adminUrl) === targetSlug) {
34732 return {
34733 title: sub.title,
34734 // Sub-menu entries inherit the parent tile's
34735 // icon — that's the dock's own convention and
34736 // avoids painting a generic glyph on a window
34737 // the user knows by its parent's identity.
34738 icon: item.icon,
34739 // `url` holds the PARENT tile's landing page, so
34740 // the new window's synthetic "back to parent"
34741 // tab links to the dock URL (themes.php) rather
34742 // than to the sub-page itself.
34743 url: item.url,
34744 multi: item.multi
34745 };
34746 }
34747 }
34748 }
34749 return null;
34750 };
34751 bindAdminLinkDispatch({
34752 adminUrl: config.adminUrl,
34753 deriveSlug: (url) => deriveWindowId(url, config.adminUrl),
34754 openWindow: (windowConfig) => {
34755 void manager2.open(windowConfig);
34756 },
34757 findDockEntry: findDockEntryForUrl2
34758 });
34759 registerNativeUrlRemap({
34760 id: "desktop-mode-posts",
34761 nativeWindowId: "desktop-mode-posts",
34762 matches: (_url, parsed) => {
34763 if (!parsed.pathname.endsWith("/edit.php")) {
34764 return false;
34765 }
34766 const postType = parsed.searchParams.get("post_type");
34767 return !postType || postType === "post";
34768 },
34769 enabled: (snapshot) => snapshot.nativePostsEnabled === true
34770 });
34771 registerNativeUrlRemap({
34772 id: "desktop-mode-pages",
34773 nativeWindowId: "desktop-mode-pages",
34774 matches: (_url, parsed) => {
34775 if (!parsed.pathname.endsWith("/edit.php")) {
34776 return false;
34777 }
34778 return parsed.searchParams.get("post_type") === "page";
34779 },
34780 enabled: (snapshot) => snapshot.nativePagesEnabled === true
34781 });
34782 registerNativeUrlRemap({
34783 id: "desktop-mode-users",
34784 nativeWindowId: "desktop-mode-users",
34785 matches: (_url, parsed) => parsed.pathname.endsWith("/users.php"),
34786 enabled: (snapshot) => snapshot.nativeUsersEnabled === true
34787 });
34788 registerNativeUrlRemap({
34789 id: "desktop-mode-user-edit",
34790 nativeWindowId: "desktop-mode-user-edit",
34791 matches: (_url, parsed) => {
34792 const path = parsed.pathname;
34793 if (path.endsWith("/profile.php")) {
34794 return true;
34795 }
34796 if (path.endsWith("/user-edit.php")) {
34797 return parsed.searchParams.has("user_id");
34798 }
34799 return false;
34800 },
34801 enabled: (snapshot) => snapshot.nativeUsersEnabled === true,
34802 onMatch: (_url, parsed) => {
34803 const userId = parseInt(
34804 parsed.searchParams.get("user_id") ?? "0",
34805 10
34806 );
34807 if (userId > 0) {
34808 setUserEditTarget(userId);
34809 }
34810 }
34811 });
34812 registerNativeUrlRemap({
34813 id: "desktop-mode-comments",
34814 nativeWindowId: "desktop-mode-comments",
34815 matches: (_url, parsed) => parsed.pathname.endsWith("/edit-comments.php"),
34816 enabled: (snapshot) => snapshot.nativeCommentsEnabled === true
34817 });
34818 registerNativeUrlRemap({
34819 id: "desktop-mode-plugins",
34820 nativeWindowId: "desktop-mode-plugins",
34821 matches: (_url, parsed) => {
34822 const path = parsed.pathname;
34823 return path.endsWith("/plugins.php") || path.endsWith("/plugin-install.php");
34824 },
34825 enabled: (snapshot) => snapshot.nativePluginsEnabled === true,
34826 onMatch: (_url, parsed) => {
34827 const tab = parsed.pathname.endsWith("/plugin-install.php") ? "browse" : "installed";
34828 void Promise.resolve().then(() => tabTarget).then((m) => {
34829 m.setPluginsWindowTab(tab);
34830 });
34831 }
34832 });
34833 if (bottomDockEl && shellEl && shellBody && config.dockItems) {
34834 desktopArea.classList.add("desktop-mode-area--with-dock");
34835 const initialLayout = osSettings.getOsSettingsSnapshot().desktopLayout;
34836 const renderIcons2 = (icons) => {
34837 renderDesktopIcons(desktopArea, icons, {
34838 openWindow: nativeWindows.openById,
34839 manager: manager2,
34840 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
34841 });
34842 };
34843 layoutDispatcher = createLayoutDispatcher(
34844 {
34845 shellRoot: shellEl,
34846 shellBody,
34847 bottomDockEl,
34848 desktopArea,
34849 windowManager: manager2,
34850 adminUrl: config.adminUrl,
34851 renderIcons: renderIcons2,
34852 getSettings: () => {
34853 const snap = osSettings.getOsSettingsSnapshot();
34854 return {
34855 itemVisibility: snap.itemVisibility,
34856 dockOrder: snap.dockOrder
34857 };
34858 }
34859 },
34860 initialLayout,
34861 config.dockItems,
34862 config.desktopIcons
34863 );
34864 layoutDispatcher.appendSystemTile(
34865 {
34866 id: OS_SETTINGS_WINDOW_ID,
34867 title: "OS Settings",
34868 icon: "dashicons-desktop",
34869 // "Open" for the dock dot means "open on the currently
34870 // active desktop." OS Settings on another desktop
34871 // shouldn't paint the dot on the active view.
34872 isOpen: () => {
34873 const win = manager2.getById(OS_SETTINGS_WINDOW_ID);
34874 if (!win) {
34875 return false;
34876 }
34877 return (win.config.desktopId || manager2.getActiveDesktopId()) === manager2.getActiveDesktopId();
34878 },
34879 onOpen: openOsSettings
34880 },
34881 "core"
34882 );
34883 if (!isStandaloneDisplay()) {
34884 layoutDispatcher.appendSystemTile(
34885 getInstallTileDef(
34886 config.pwa?.appName || "WordPress",
34887 showToast
34888 ),
34889 "core"
34890 );
34891 }
34892 window.matchMedia("(display-mode: standalone)").addEventListener("change", (e) => {
34893 if (e.matches) {
34894 layoutDispatcher?.removeSystemTile(
34895 "desktop-mode-pwa-install"
34896 );
34897 }
34898 });
34899 void isLikelyInstalled().then((installed2) => {
34900 if (installed2) {
34901 layoutDispatcher?.removeSystemTile(
34902 "desktop-mode-pwa-install"
34903 );
34904 }
34905 });
34906 }
34907 function openOsSettings(opts = {}) {
34908 if (opts.tabId === "extended") {
34909 opts = { ...opts, tabId: "features" };
34910 }
34911 if (opts.tabId) {
34912 osSettings.activeTabId = opts.tabId;
34913 }
34914 void manager2.open({
34915 id: OS_SETTINGS_WINDOW_ID,
34916 baseId: OS_SETTINGS_WINDOW_ID,
34917 url: "#os-settings",
34918 title: "OS Settings",
34919 icon: "dashicons-desktop",
34920 native: true,
34921 render: (body) => osSettings.renderPanel(body),
34922 width: 820,
34923 height: 720,
34924 minWidth: 560,
34925 minHeight: 480
34926 });
34927 if (opts.tabId) {
34928 osSettings.focusTab(opts.tabId);
34929 }
34930 }
34931 function openBugReport() {
34932 void manager2.open({
34933 id: BUG_REPORT_WINDOW_ID,
34934 baseId: BUG_REPORT_WINDOW_ID,
34935 url: `#${BUG_REPORT_WINDOW_ID}`,
34936 title: "Report a bug",
34937 icon: "dashicons-buddicons-replies",
34938 native: true,
34939 render: (body) => renderBugReport(body),
34940 width: 560,
34941 height: 620,
34942 minWidth: 420,
34943 minHeight: 480
34944 });
34945 }
34946 document.addEventListener("desktop-mode-open-bug-report", () => {
34947 openBugReport();
34948 });
34949 if (layoutDispatcher) {
34950 layoutDispatcher.appendSystemTile(
34951 {
34952 id: BUG_REPORT_WINDOW_ID,
34953 title: "Report a bug",
34954 icon: "dashicons-buddicons-replies",
34955 isOpen: () => {
34956 const win = manager2.getById(BUG_REPORT_WINDOW_ID);
34957 if (!win) {
34958 return false;
34959 }
34960 return (win.config.desktopId || manager2.getActiveDesktopId()) === manager2.getActiveDesktopId();
34961 },
34962 onOpen: openBugReport
34963 },
34964 "core"
34965 );
34966 layoutDispatcher.appendSystemTile(
34967 getExitDesktopModeTileDef(),
34968 "core"
34969 );
34970 }
34971 const dock = layoutDispatcher?.getPrimary() ?? null;
34972 void syncNativeWindows(
34973 Array.isArray(config.nativeWindows) ? config.nativeWindows : []
34974 );
34975 const hasSession = hasRestorableSession(config.session);
34976 const sessionRestore = hasSession ? restoreSession(manager2, config, desktopArea).catch((err) => {
34977 if (typeof console !== "undefined") {
34978 console.error("[desktop-mode] session restore failed:", err);
34979 }
34980 }) : Promise.resolve();
34981 const defaultEnabled = config.defaultWindow?.enabled !== false;
34982 const defaultUrlEarly = config.defaultWindow?.url ?? "";
34983 const isNativeDefault = typeof defaultUrlEarly === "string" && defaultUrlEarly.startsWith("native:");
34984 if (shouldAutoOpenCurrentPage({
34985 fromPortal: config.fromPortal,
34986 fromPortalIntent: config.fromPortalIntent,
34987 hasSession,
34988 defaultEnabled,
34989 isNativeDefault
34990 })) {
34991 void sessionRestore.then(
34992 () => openCurrentPage(manager2, config).catch((err) => {
34993 if (typeof console !== "undefined") {
34994 console.error("[desktop-mode] openCurrentPage failed:", err);
34995 }
34996 })
34997 );
34998 }
34999 const saveSession = createSessionSaver(manager2, config);
35000 wireSessionEvents(saveSession);
35001 const setDefaultWindow = async (url) => {
35002 try {
35003 const response = await trackedFetch(
35004 manager2,
35005 config.defaultWindowUrl,
35006 {
35007 method: "POST",
35008 credentials: "same-origin",
35009 headers: {
35010 "Content-Type": "application/json",
35011 "X-WP-Nonce": config.restNonce
35012 },
35013 body: JSON.stringify({ url })
35014 },
35015 { source: "desktop-mode/default-window" }
35016 );
35017 if (!response.ok) {
35018 throw new Error(`HTTP ${response.status}`);
35019 }
35020 const data = await response.json();
35021 config.defaultWindow = data;
35022 document.dispatchEvent(
35023 new CustomEvent("desktop-mode-default-window-changed", {
35024 detail: data
35025 })
35026 );
35027 } catch (err) {
35028 doAction(HOOKS.SHELL_ERROR, { scope: "default-window-save", error: err });
35029 if (typeof console !== "undefined") {
35030 console.error(
35031 "[desktop-mode] Failed to save default window:",
35032 err
35033 );
35034 }
35035 }
35036 };
35037 manager2.onToggleStartupRequested = (win) => {
35038 const currentPref = config.defaultWindow;
35039 const isNative = !!win.config.native;
35040 const winValue = isNative ? `native:${win.id}` : win.getCurrentUrl();
35041 const matchesCurrent = isNative ? currentPref?.url === winValue : urlMatchKey(currentPref?.url ?? "") === urlMatchKey(winValue);
35042 const alreadyDefault = !!currentPref?.enabled && matchesCurrent;
35043 void setDefaultWindow(alreadyDefault ? null : winValue);
35044 };
35045 if (config.defaultWindow?.enabled && config.fromPortal && !config.fromPortalIntent && !hasSession && isNativeDefault) {
35046 const nativeId = defaultUrlEarly.slice("native:".length);
35047 queueMicrotask(() => {
35048 if (nativeId === OS_SETTINGS_WINDOW_ID) {
35049 openOsSettings();
35050 return;
35051 }
35052 void nativeWindows.openById(nativeId);
35053 });
35054 }
35055 const placeSystemTile = (item) => {
35056 layoutDispatcher?.appendSystemTile(item);
35057 };
35058 const syncServerWidgets = createWidgetRegistrySync({
35059 layer: widgetLayer
35060 });
35061 void syncServerWidgets(
35062 Array.isArray(config.serverWidgets) ? config.serverWidgets : []
35063 );
35064 const syncServerWallpapers = createWallpaperRegistrySync({
35065 osSettings
35066 });
35067 void syncServerWallpapers(
35068 Array.isArray(config.serverWallpapers) ? config.serverWallpapers : []
35069 );
35070 const syncServerGames = createGamesRegistrySync();
35071 void syncServerGames(
35072 Array.isArray(config.serverGames) ? config.serverGames : []
35073 );
35074 const syncServerCommands = createCommandRegistrySync();
35075 void syncServerCommands(
35076 Array.isArray(config.serverCommandScripts) ? config.serverCommandScripts : [],
35077 Array.isArray(config.serverCommands) ? config.serverCommands : []
35078 );
35079 const syncServerSettingsTabs = createSettingsTabRegistrySync();
35080 void syncServerSettingsTabs(
35081 Array.isArray(config.serverSettingsTabScripts) ? config.serverSettingsTabScripts : [],
35082 Array.isArray(config.serverSettingsTabs) ? config.serverSettingsTabs : []
35083 );
35084 const syncServerTitleBarButtons = createTitleBarButtonRegistrySync();
35085 void syncServerTitleBarButtons(
35086 Array.isArray(config.serverTitleBarButtonScripts) ? config.serverTitleBarButtonScripts : []
35087 );
35088 const syncServerUnfocusEffects = createUnfocusEffectRegistrySync();
35089 void syncServerUnfocusEffects(
35090 Array.isArray(config.serverUnfocusEffectScripts) ? config.serverUnfocusEffectScripts : []
35091 );
35092 const syncServerWindowLinkRenderers = createWindowLinkRendererRegistrySync();
35093 void syncServerWindowLinkRenderers(
35094 Array.isArray(config.serverWindowLinkRendererScripts) ? config.serverWindowLinkRendererScripts : []
35095 );
35096 startUnfocusEngine({ manager: manager2, osSettings });
35097 startWindowLinksEngine({ manager: manager2 });
35098 startWindowLinkRenderHost({ manager: manager2, osSettings });
35099 bootRelatedEntities({
35100 manager: manager2,
35101 openUrl: (item) => {
35102 const relatedId = deriveWindowId(item.url, config.adminUrl);
35103 void manager2.open({
35104 id: relatedId,
35105 baseId: relatedId,
35106 url: item.url,
35107 title: item.label,
35108 icon: item.icon || "dashicons-admin-links"
35109 });
35110 }
35111 });
35112 const syncServerDockRailRenderers = createDockRailRendererSync();
35113 void syncServerDockRailRenderers(
35114 Array.isArray(config.serverDockRailRendererScripts) ? config.serverDockRailRendererScripts : []
35115 );
35116 const syncServerWindowThemes = createWindowThemeRegistrySync();
35117 void syncServerWindowThemes(
35118 Array.isArray(config.serverWindowThemeScripts) ? config.serverWindowThemeScripts : [],
35119 Array.isArray(config.serverWindowThemes) ? config.serverWindowThemes : []
35120 );
35121 registerBuiltInControls();
35122 const syncServerWindowControls = createWindowControlRegistrySync();
35123 void syncServerWindowControls(
35124 Array.isArray(config.serverWindowControlScripts) ? config.serverWindowControlScripts : [],
35125 Array.isArray(config.serverWindowControls) ? config.serverWindowControls : []
35126 );
35127 const syncServerWindowSlots = createWindowSlotRegistrySync();
35128 void syncServerWindowSlots(
35129 Array.isArray(config.serverWindowSlotScripts) ? config.serverWindowSlotScripts : [],
35130 Array.isArray(config.serverWindowSlots) ? config.serverWindowSlots : []
35131 );
35132 applyServerWindowNotices(
35133 Array.isArray(config.serverWindowNotices) ? config.serverWindowNotices : []
35134 );
35135 const syncServerWindowChromes = createWindowChromeRegistrySync();
35136 void syncServerWindowChromes(
35137 Array.isArray(config.serverWindowChromeScripts) ? config.serverWindowChromeScripts : [],
35138 Array.isArray(config.serverWindowChromes) ? config.serverWindowChromes : []
35139 );
35140 const connectionBridge = createConnectionBridge(manager2);
35141 attachBroadcastBus(manager2);
35142 scheduleIdleBoot(() => installBroadcastReceiver());
35143 installWindowLoadingTransitions();
35144 addAction(
35145 "desktop-mode.shell.toast",
35146 "desktop-mode/shell-toast",
35147 (payload) => {
35148 if (!payload || typeof payload.message !== "string") {
35149 return;
35150 }
35151 showToast({
35152 message: payload.message,
35153 action: payload.action,
35154 duration: payload.duration
35155 });
35156 }
35157 );
35158 const cfgWithBin = config;
35159 const cfgCountRaw = cfgWithBin.recycleBinCount;
35160 startRecycleBinBadge(
35161 Number(cfgCountRaw) || 0,
35162 typeof cfgWithBin.recycleBinCountUrl === "string" ? cfgWithBin.recycleBinCountUrl : ""
35163 );
35164 registerBuiltInPeekRenderers({
35165 getRecycleBinCount: _currentRecycleBinBadge
35166 });
35167 window.__desktopModeConnectionBridge = connectionBridge;
35168 addAction(HOOKS.WINDOW_CLOSED, "desktop-mode/connection-cleanup", (e) => {
35169 if (e?.windowId) {
35170 connectionBridge.onWindowClosed(e.windowId);
35171 }
35172 });
35173 addAction(HOOKS.IFRAME_READY, "desktop-mode/connection-rearm", (e) => {
35174 if (e?.windowId) {
35175 connectionBridge.onIframeReady(e.windowId);
35176 }
35177 });
35178 const registerWindow = createRegisterWindow(manager2);
35179 const renderIcons = (icons) => {
35180 if (layoutDispatcher) {
35181 layoutDispatcher.applyDesktopIcons(icons);
35182 return;
35183 }
35184 renderDesktopIcons(desktopArea, icons, {
35185 openWindow: nativeWindows.openById,
35186 manager: manager2,
35187 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
35188 });
35189 };
35190 const refreshMenu = bindMenuRefresh({
35191 layoutDispatcher,
35192 desktopArea,
35193 config,
35194 syncNativeWindows,
35195 syncServerWidgets,
35196 syncServerWallpapers,
35197 syncServerCommands,
35198 syncServerSettingsTabs,
35199 syncServerTitleBarButtons,
35200 syncServerUnfocusEffects,
35201 syncServerWindowLinkRenderers,
35202 syncServerDockRailRenderers,
35203 syncServerGames,
35204 renderIcons,
35205 syncShortcuts: () => {
35206 const snapshot = osSettings.getOsSettingsSnapshot();
35207 syncShortcutsWithVisibility(
35208 snapshot.itemVisibility,
35209 snapshot.dockPromotedPositions,
35210 snapshot.desktopLayout
35211 );
35212 }
35213 });
35214 osSettings.subscribeOsSettings((snapshot) => {
35215 if (!layoutDispatcher) {
35216 return;
35217 }
35218 const prevLayout = layoutDispatcher.getLayout();
35219 layoutDispatcher.setLayout(snapshot.desktopLayout);
35220 desktopApi.dock = layoutDispatcher.getPrimary();
35221 desktopApi.sideDock = layoutDispatcher.getSide();
35222 desktopApi.desktopLayout = snapshot.desktopLayout;
35223 if (prevLayout === snapshot.desktopLayout) {
35224 layoutDispatcher.refresh();
35225 }
35226 syncShortcutsWithVisibility(
35227 snapshot.itemVisibility,
35228 snapshot.dockPromotedPositions,
35229 snapshot.desktopLayout
35230 );
35231 setCurrentLayout(snapshot.desktopLayout);
35232 });
35233 installShortcutsSync(
35234 () => osSettings.getOsSettingsSnapshot().itemVisibility,
35235 () => osSettings.getOsSettingsSnapshot().dockPromotedPositions,
35236 () => osSettings.getOsSettingsSnapshot().desktopLayout
35237 );
35238 setCurrentLayout(osSettings.getOsSettingsSnapshot().desktopLayout);
35239 const desktopApi = buildPublicApi({
35240 manager: manager2,
35241 dock,
35242 layoutDispatcher,
35243 osSettings,
35244 iconsApi,
35245 filesApi,
35246 saveSession,
35247 widgetLayer,
35248 registerWindow,
35249 openWindowById: nativeWindows.openById,
35250 openNewWindowById: nativeWindows.openNewById,
35251 placeSystemTile,
35252 setDefaultWindow,
35253 refreshMenu,
35254 openOsSettings,
35255 aiAssistant,
35256 dragBridge,
35257 dragManager,
35258 connect: connectionBridge.connect,
35259 getConnection: connectionBridge.getConnection,
35260 wallpaperSuspend: {
35261 suspend: (reason) => wallpaperLayer?.suspend(reason),
35262 resume: (reason) => wallpaperLayer?.resume(reason),
35263 isSuspended: () => wallpaperLayer?.isSuspended() ?? false
35264 },
35265 config
35266 });
35267 installPublicApi(desktopApi);
35268 scheduleIdleBoot(() => installRecycleBinDropTargets(dragManager));
35269 bootHeartbeatBus();
35270 bootGamesChallenges({
35271 currentUserId: Number(config.currentUserId) || 0
35272 });
35273 scheduleIdleBoot(() => bootContentChangesHeartbeat());
35274 scheduleIdleBoot(() => bootNonceRefresh());
35275 scheduleIdleBoot(
35276 () => bootAuthRecovery({
35277 currentUserId: Number(config.currentUserId) || 0
35278 })
35279 );
35280 bootStickyNotes({
35281 host: desktopArea,
35282 config,
35283 // Only boot when the Gutenberg Guidelines experiment is live
35284 // server-side; otherwise the layer's REST probes would 404. The
35285 // flag is `undefined` on shells older than the one that added it
35286 // → the layer treats that as available (boot and swallow).
35287 available: config.stickyNotes?.available,
35288 getActiveDesktopId: () => manager2.getActiveDesktopId(),
35289 openArtifact: (url, title) => {
35290 const id = deriveWindowId(url, config.adminUrl);
35291 void manager2.open({
35292 id,
35293 baseId: id,
35294 url,
35295 title,
35296 icon: "dashicons-edit-page"
35297 });
35298 },
35299 onError: (message) => {
35300 showToast({ message });
35301 }
35302 });
35303 bootNotes({
35304 host: desktopArea,
35305 config,
35306 onError: (message) => {
35307 showToast({ message });
35308 }
35309 });
35310 installOpenDeps({
35311 openUrl: ({ id, url, title, icon }) => {
35312 if (tryNativeUrlRemap(url)) {
35313 return true;
35314 }
35315 void manager2.open({ id, baseId: id, url, title, icon });
35316 return true;
35317 },
35318 openNativeWindow: (id) => nativeWindows.openById(id),
35319 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
35320 });
35321 setUserAssociations(
35322 config.userFileAssociations ?? {}
35323 );
35324 void maybeShowUpdate({
35325 update: config.coreUpdate,
35326 openUrl: ({ url, title }) => {
35327 if (tryNativeUrlRemap(url)) {
35328 return;
35329 }
35330 void manager2.open({
35331 id: "update-core",
35332 baseId: "update-core",
35333 url,
35334 title,
35335 icon: "dashicons-update"
35336 });
35337 }
35338 });
35339 const openNoticeUrl = ({ url, title }) => {
35340 if (tryNativeUrlRemap(url)) {
35341 return;
35342 }
35343 const baseId = deriveWindowId(url, config.adminUrl);
35344 void manager2.open({
35345 id: baseId,
35346 baseId,
35347 url,
35348 title,
35349 icon: "dashicons-info"
35350 });
35351 };
35352 maybeShowNotices({ notices: config.coreNotices, openUrl: openNoticeUrl });
35353 maybeShowNotices({
35354 notices: config.pluginNotices,
35355 openUrl: openNoticeUrl,
35356 keyPrefix: "plugin-notice"
35357 });
35358 if (typeof config.filesUrl === "string" && config.filesUrl) {
35359 installRestDeps({
35360 baseUrl: config.filesUrl,
35361 nonce: config.restNonce
35362 });
35363 const rootHost = document.getElementById("desktop-mode-area");
35364 if (rootHost) {
35365 const layerHandle = mountFilesLayer(rootHost, 0);
35366 const reveal = () => {
35367 if (!desktopArea.classList.contains("desktop-mode-area--booting")) {
35368 return;
35369 }
35370 requestAnimationFrame(() => {
35371 desktopArea.classList.remove("desktop-mode-area--booting");
35372 });
35373 };
35374 const safetyTimer = setTimeout(reveal, 2e3);
35375 void layerHandle.hydrated.then(() => {
35376 clearTimeout(safetyTimer);
35377 reveal();
35378 });
35379 }
35380 }
35381 scheduleIdleBoot(() => startFilesHeartbeat());
35382 scheduleIdleBoot(() => startFilesRestoreSync());
35383 scheduleIdleBoot(() => bootPresenceProbe());
35384 doAction(HOOKS.COMPONENTS_REGISTERED, { tags: [...WPD_COMPONENT_TAGS] });
35385 registerBuiltInCommands();
35386 bootstrapPwa(config, showToast);
35387 const overlayPreload = () => {
35388 preloadShellOverlays(config.shellOverlaysBundleUrl ?? "");
35389 preloadWindowSystem(config.windowSystemBundleUrl ?? "");
35390 };
35391 if (typeof window.requestIdleCallback === "function") {
35392 window.requestIdleCallback(overlayPreload, { timeout: 1500 });
35393 } else {
35394 window.setTimeout(overlayPreload, 0);
35395 }
35396 doAction(HOOKS.INIT, { config });
35397 _earlyReady = true;
35398 const queued = _earlyReadyQueue.splice(0);
35399 for (const cb of queued) {
35400 try {
35401 cb();
35402 } catch (err) {
35403 doAction(HOOKS.SHELL_ERROR, {
35404 scope: "when-ready-cb",
35405 error: err
35406 });
35407 if (typeof console !== "undefined") {
35408 console.error("[desktop-mode] whenReady cb threw:", err);
35409 }
35410 }
35411 }
35412 osSettings.apply();
35413 widgetLayer?.hydrate();
35414 window.addEventListener("pagehide", () => {
35415 wallpaperLayer?.teardownActive();
35416 widgetLayer?.disposeAll();
35417 });
35418 bindShellLifecycle();
35419 bindTopWindowLinkInterceptor(manager2, config);
35420 const relayoutRoot = (transform, persist2 = true) => {
35421 const root = filesApi.store.getState().placementsByFolder.get(0) ?? [];
35422 const ordered = transform(root);
35423 const rowsPerCol = Math.max(
35424 1,
35425 Math.floor((desktopArea.clientHeight - 16) / 110)
35426 );
35427 const occupied = /* @__PURE__ */ new Set();
35428 let i = 0;
35429 for (const p of ordered) {
35430 const cell = snapToEmptyCell(
35431 16 + Math.floor(i / rowsPerCol) * 96,
35432 16 + i % rowsPerCol * 110,
35433 occupied,
35434 desktopArea
35435 );
35436 occupied.add(`${cell.col},${cell.row}`);
35437 i++;
35438 if (p.x === cell.x && p.y === cell.y) {
35439 continue;
35440 }
35441 filesApi.store.upsertPlacement({
35442 ...p,
35443 x: cell.x,
35444 y: cell.y,
35445 sortOrder: i
35446 });
35447 if (!persist2 || isSyntheticPlacement(p)) {
35448 continue;
35449 }
35450 void updatePlacement(p.id, {
35451 x: cell.x,
35452 y: cell.y,
35453 sortOrder: i
35454 }).catch((err) => {
35455 console.error("[desktop-mode] relayout persist failed", err);
35456 });
35457 }
35458 };
35459 const rootSortTransform = (mode) => (arr) => {
35460 const sorted = arr.slice();
35461 switch (mode) {
35462 case "name-asc":
35463 sorted.sort(
35464 (a, b) => a.file.title.localeCompare(b.file.title)
35465 );
35466 break;
35467 case "name-desc":
35468 sorted.sort(
35469 (a, b) => b.file.title.localeCompare(a.file.title)
35470 );
35471 break;
35472 case "date-asc":
35473 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
35474 break;
35475 case "date-desc":
35476 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
35477 break;
35478 }
35479 return sorted;
35480 };
35481 const ROOT_SORT_MODE_KEY = "desktop-mode:root-sort-mode";
35482 const isRootSortMode = (v) => v === "name-asc" || v === "name-desc" || v === "date-asc" || v === "date-desc";
35483 let rootSortMode = (() => {
35484 try {
35485 const raw = window.localStorage.getItem(ROOT_SORT_MODE_KEY);
35486 return isRootSortMode(raw) ? raw : null;
35487 } catch {
35488 return null;
35489 }
35490 })();
35491 const setRootSortMode = (mode) => {
35492 rootSortMode = mode;
35493 try {
35494 if (mode) {
35495 window.localStorage.setItem(ROOT_SORT_MODE_KEY, mode);
35496 } else {
35497 window.localStorage.removeItem(ROOT_SORT_MODE_KEY);
35498 }
35499 } catch {
35500 }
35501 };
35502 addAction(
35503 "desktop-mode.files.tile-manually-placed",
35504 "desktop-mode/root-sort-clear",
35505 (payload) => {
35506 const folderId = payload?.folderId;
35507 if (folderId === 0) {
35508 setRootSortMode(null);
35509 }
35510 }
35511 );
35512 if (typeof ResizeObserver !== "undefined") {
35513 let lastW = desktopArea.clientWidth;
35514 let lastH = desktopArea.clientHeight;
35515 const ro = new ResizeObserver(() => {
35516 if (!rootSortMode) {
35517 return;
35518 }
35519 const w = desktopArea.clientWidth;
35520 const h = desktopArea.clientHeight;
35521 if (w === lastW && h === lastH) {
35522 return;
35523 }
35524 lastW = w;
35525 lastH = h;
35526 relayoutRoot(rootSortTransform(rootSortMode), false);
35527 });
35528 ro.observe(desktopArea);
35529 }
35530 let pointerdownOnWallpaper = false;
35531 desktopArea.addEventListener("pointerdown", (e) => {
35532 if (!e.isPrimary) {
35533 return;
35534 }
35535 pointerdownOnWallpaper = e.target === desktopArea;
35536 });
35537 desktopArea.addEventListener("click", (e) => {
35538 if (!osSettings.state.showDesktopOnWallpaperClick) {
35539 return;
35540 }
35541 if (e.target !== desktopArea) {
35542 return;
35543 }
35544 if (!pointerdownOnWallpaper) {
35545 return;
35546 }
35547 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
35548 return;
35549 }
35550 if (isWallpaperMenuOpen()) {
35551 return;
35552 }
35553 if (dragManager.recentlyEndedDrag()) {
35554 return;
35555 }
35556 manager2.toggleShowDesktop();
35557 });
35558 desktopArea.addEventListener("contextmenu", (e) => {
35559 if (e.target !== desktopArea) {
35560 return;
35561 }
35562 e.preventDefault();
35563 const clientX = e.clientX;
35564 const clientY = e.clientY;
35565 (() => {
35566 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
35567 return;
35568 }
35569 if (isWallpaperMenuOpen()) {
35570 closeWallpaperMenu();
35571 return;
35572 }
35573 const dropClient = { x: clientX, y: clientY };
35574 const cellAtClick = () => {
35575 const rect = desktopArea.getBoundingClientRect();
35576 const rawX = Math.max(0, dropClient.x - rect.left);
35577 const rawY = Math.max(0, dropClient.y - rect.top);
35578 const occupied = buildOccupiedSet(
35579 filesApi.store.getState().placementsByFolder.get(0) ?? []
35580 );
35581 return snapToEmptyCell(rawX, rawY, occupied, desktopArea);
35582 };
35583 const createUrlPlacement = (dialogTitle, description) => {
35584 openUrlDialog({
35585 title: dialogTitle,
35586 description,
35587 nameLabel: "Name",
35588 urlLabel: "URL",
35589 submitLabel: "Create",
35590 onSubmit: async ({ name, url }) => {
35591 const cell = cellAtClick();
35592 const placement = await createPlacement({
35593 type: "link",
35594 ref: url,
35595 parentId: 0,
35596 x: cell.x,
35597 y: cell.y,
35598 meta: name ? { name } : void 0
35599 });
35600 filesApi.store.upsertPlacement(placement);
35601 }
35602 });
35603 };
35604 const items = buildMenuItems({
35605 createFolder: () => {
35606 openCreateFolderDialog({
35607 onSubmit: async (name) => {
35608 const folder = await createFolder({ name });
35609 const cell = cellAtClick();
35610 const placement = await createPlacement({
35611 type: "folder",
35612 ref: String(folder.id),
35613 parentId: 0,
35614 x: cell.x,
35615 y: cell.y
35616 });
35617 filesApi.store.upsertFolder(folder);
35618 filesApi.store.upsertPlacement(placement);
35619 }
35620 });
35621 },
35622 createUrl: () => createUrlPlacement(
35623 "New URL",
35624 "Opens the URL in a new browser tab."
35625 ),
35626 toggleShowDesktop: () => manager2.toggleShowDesktop(),
35627 openOsSettings: () => openOsSettings(),
35628 sortIcons: (mode) => {
35629 setRootSortMode(mode);
35630 relayoutRoot(rootSortTransform(mode));
35631 },
35632 currentSortMode: rootSortMode,
35633 includeShowDesktop: !osSettings.state.showDesktopOnWallpaperClick,
35634 labels: {
35635 createFolder: "New folder",
35636 showDesktop: "Show desktop",
35637 osSettings: "OS Settings",
35638 sortHeading: "Sort by",
35639 sortNameAsc: "Name (A → Z)",
35640 sortNameDesc: "Name (Z → A)",
35641 sortDateAsc: "Date (oldest first)",
35642 sortDateDesc: "Date (newest first)",
35643 newUrl: "New URL"
35644 },
35645 serverItems: config.serverWallpaperMenuItems ?? []
35646 });
35647 openWallpaperMenu(
35648 document.body,
35649 { x: clientX, y: clientY },
35650 items
35651 );
35652 })();
35653 });
35654 void Promise.resolve().then(() => index).then((mod) => {
35655 mod.bootOsFileDrop({
35656 config: config.dropConfig,
35657 mediaUrl: config.mediaUrl,
35658 restNonce: config.restNonce,
35659 filesUrl: config.filesUrl,
35660 storage: config.desktopStorage
35661 });
35662 });
35663 document.dispatchEvent(
35664 new CustomEvent("desktop-mode-init", {
35665 detail: { config, restored: hasSession }
35666 })
35667 );
35668 }
35669 startMissingImportWarner();
35670 if (document.readyState === "loading") {
35671 document.addEventListener("DOMContentLoaded", init);
35672 } else {
35673 init();
35674 }
35675 const FILE_DROP_HOOKS = {
35676 /**
35677 * Filter — fires once per drop, after the manager has parsed
35678 * the OS `DataTransfer` into `File[]` and BEFORE the mime /
35679 * size filter runs.
35680 *
35681 * Signature: `(files: File[], ctx: DropContext) => File[]`.
35682 * Return an empty array to abort the drop silently.
35683 */
35684 FILES_DETECTED: "desktop-mode.drop.files-detected",
35685 /**
35686 * Action — fires after the mime / size filter has rejected
35687 * one or more files. Payload: `{ rejections: DropRejection[],
35688 * context: DropContext }`. The shell toasts a default message;
35689 * subscribers can surface a custom UX (a side panel with the
35690 * list, an analytics call).
35691 */
35692 FILES_REJECTED: "desktop-mode.drop.files-rejected",
35693 /**
35694 * Filter — fires per file before the upload dialog renders.
35695 * Receives `DropFileEntry` (the underlying file + the
35696 * manager's default `fields`). Mutate `fields` (or return a
35697 * new object) to change what the user sees in the form.
35698 *
35699 * Signature: `(entry: DropFileEntry, ctx: DropContext)
35700 * => DropFileEntry`.
35701 */
35702 DIALOG_FIELDS: "desktop-mode.drop.dialog-fields",
35703 /**
35704 * Filter — last call before the manager `POST`s to
35705 * `wp/v2/media`. Receives `{ file: File, fields:
35706 * DropDialogFields, mime: string }`. Return `null` to cancel
35707 * the upload entirely (e.g. a plugin handled it via a
35708 * different endpoint).
35709 *
35710 * Signature: `(payload, ctx: DropContext) => payload | null`.
35711 */
35712 BEFORE_UPLOAD: "desktop-mode.drop.before-upload",
35713 /**
35714 * Action — fires once `BEFORE_UPLOAD` has cleared and the XHR
35715 * is `open()`ed, immediately before `send()`. Payload:
35716 * `{ file: File, fields: DropDialogFields, context: DropContext,
35717 * abort: () => void }`. The `abort` handle aborts the in-flight
35718 * request; the manager rejects with `UploadAbortedError` and
35719 * fires `UPLOAD_FAILED` with that error.
35720 *
35721 * Pair with `UPLOAD_PROGRESS` to drive a progress UI; pair with
35722 * `AFTER_UPLOAD` / `UPLOAD_FAILED` to know when the upload ends.
35723 *
35724 * @since 0.31.0
35725 */
35726 UPLOAD_STARTED: "desktop-mode.drop.upload-started",
35727 /**
35728 * Action — fires for every `XMLHttpRequestUpload.progress` event.
35729 * Payload: `{ file: File, fields: DropDialogFields, context:
35730 * DropContext, loaded: number, total: number, indeterminate:
35731 * boolean }`. `total` is `0` and `indeterminate` is `true` when
35732 * the request body length isn't known (rare for multipart, but
35733 * possible on transcoding proxies); subscribers should treat
35734 * that as an indeterminate state.
35735 *
35736 * A synthetic 100%-loaded event is dispatched once the `upload`
35737 * stream emits `load` so a HUD can show a definite "wrapping up"
35738 * state while the server finishes the response.
35739 *
35740 * @since 0.31.0
35741 */
35742 UPLOAD_PROGRESS: "desktop-mode.drop.upload-progress",
35743 /**
35744 * Action — fires after a successful upload. Payload:
35745 * `{ file: File, result: DropUploadResult, fields:
35746 * DropDialogFields, context: DropContext }`.
35747 *
35748 * The `file` field carries the same `File` reference that
35749 * `UPLOAD_STARTED` / `UPLOAD_PROGRESS` exposed (i.e. the
35750 * payload returned by the `BEFORE_UPLOAD` filter, in case a
35751 * plugin swapped the file). Subscribers tracking per-file
35752 * state — progress HUDs, sequence counters — should match on
35753 * this identity rather than the filename: two drops of
35754 * `photo.jpg` from different folders would otherwise route
35755 * each other's success event to the wrong row.
35756 *
35757 * @since 0.31.0 the `file` field was added; pre-0.31.0 code
35758 * that destructured `{ result, fields, context }` keeps working.
35759 */
35760 AFTER_UPLOAD: "desktop-mode.drop.after-upload",
35761 /**
35762 * Action — fires after an upload fails. Payload:
35763 * `{ file: File, error: Error, context: DropContext }`.
35764 * `error` is an `UploadAbortedError` when the failure came
35765 * from the caller invoking the `abort()` handle on
35766 * `UPLOAD_STARTED`.
35767 *
35768 * `file` carries the same identity as `UPLOAD_STARTED` /
35769 * `UPLOAD_PROGRESS` / `AFTER_UPLOAD` — the post-`BEFORE_UPLOAD`
35770 * `File`, in case a plugin swapped it. Match by reference, not
35771 * filename: a HUD that keys its row map on the started-File
35772 * needs the same key here, otherwise the row stays stuck in
35773 * "running" after a failure when a `BEFORE_UPLOAD` filter
35774 * replaced the file.
35775 */
35776 UPLOAD_FAILED: "desktop-mode.drop.upload-failed"
35777 };
35778 const MAX_DEPTH = 32;
35779 function snapshotEntries(items) {
35780 if (!items) {
35781 return [];
35782 }
35783 const out = [];
35784 for (let i = 0; i < items.length; i++) {
35785 const item = items[i];
35786 if (item.kind !== "file") {
35787 continue;
35788 }
35789 const entry = typeof item.webkitGetAsEntry === "function" ? item.webkitGetAsEntry() : null;
35790 if (entry) {
35791 out.push(entry);
35792 }
35793 }
35794 return out;
35795 }
35796 function readAllEntries(dir) {
35797 const reader = dir.createReader();
35798 return new Promise((resolve2, reject) => {
35799 const out = [];
35800 const step = () => {
35801 reader.readEntries((batch) => {
35802 if (batch.length === 0) {
35803 resolve2(out);
35804 return;
35805 }
35806 out.push(...batch);
35807 step();
35808 }, reject);
35809 };
35810 step();
35811 });
35812 }
35813 function entryFile(entry) {
35814 return new Promise((resolve2, reject) => {
35815 entry.file(resolve2, reject);
35816 });
35817 }
35818 async function collectDroppedTree(entries) {
35819 const collection = {
35820 files: [],
35821 emptyDirs: [],
35822 hadDirectory: false
35823 };
35824 for (const entry of entries) {
35825 await collectEntry(entry, "", collection, 0);
35826 }
35827 return collection;
35828 }
35829 async function collectEntry(entry, prefix, collection, depth) {
35830 if (depth > MAX_DEPTH) {
35831 return;
35832 }
35833 if (entry.isFile) {
35834 try {
35835 const file = await entryFile(entry);
35836 collection.files.push({
35837 file,
35838 // `File.webkitRelativePath` is EMPTY for drag-dropped
35839 // files — the path must come from the entry walk.
35840 relativePath: prefix ? `${prefix}${file.name}` : ""
35841 });
35842 } catch {
35843 }
35844 return;
35845 }
35846 if (!entry.isDirectory) {
35847 return;
35848 }
35849 collection.hadDirectory = true;
35850 const dirPath = `${prefix}${entry.name}`;
35851 let children = [];
35852 try {
35853 children = await readAllEntries(entry);
35854 } catch {
35855 children = [];
35856 }
35857 if (children.length === 0) {
35858 collection.emptyDirs.push(dirPath);
35859 return;
35860 }
35861 for (const child of children) {
35862 await collectEntry(child, `${dirPath}/`, collection, depth + 1);
35863 }
35864 }
35865 const IFRAME_PASSTHROUGH_SELECTORS = [
35866 ".components-drop-zone",
35867 "[data-drop-zone]",
35868 ".uploader-window",
35869 ".media-frame-content"
35870 ];
35871 function dragHasFiles(ev) {
35872 const types = ev.dataTransfer?.types;
35873 if (!types) {
35874 return false;
35875 }
35876 const list2 = types;
35877 if (typeof list2.includes === "function") {
35878 return list2.includes("Files");
35879 }
35880 if (typeof list2.contains === "function") {
35881 return list2.contains("Files");
35882 }
35883 for (let i = 0; i < list2.length; i++) {
35884 if (list2[i] === "Files") {
35885 return true;
35886 }
35887 }
35888 return false;
35889 }
35890 function windowIdFromElement(el) {
35891 const root = el?.closest?.(".desktop-mode-window");
35892 if (!root) {
35893 return void 0;
35894 }
35895 const m = /^wp-window-(.+)$/.exec(root.id || "");
35896 return m ? m[1] : void 0;
35897 }
35898 function resolveWindowIdFromSource(source) {
35899 if (!source) {
35900 return void 0;
35901 }
35902 const iframes = document.querySelectorAll("iframe");
35903 for (const f of Array.from(iframes)) {
35904 if (f.contentWindow === source) {
35905 const fromRoot = windowIdFromElement(f);
35906 if (fromRoot) {
35907 return fromRoot;
35908 }
35909 const host = f.closest("[data-window-id]");
35910 return host?.getAttribute("data-window-id") || void 0;
35911 }
35912 }
35913 return void 0;
35914 }
35915 function mountOsFileDropManager(opts) {
35916 const host = window;
35917 if (host.__desktopModeOsFileDropMounted) {
35918 return host.__desktopModeOsFileDropMounted;
35919 }
35920 if (!opts.config.enabled) {
35921 return mountNoOp();
35922 }
35923 const overlayEl = ensureDropOverlay();
35924 let dragDepth = 0;
35925 let dragWatchdog = null;
35926 const resetOverlay = () => {
35927 dragDepth = 0;
35928 overlayEl.classList.remove("is-active");
35929 if (dragWatchdog !== null) {
35930 clearTimeout(dragWatchdog);
35931 dragWatchdog = null;
35932 }
35933 };
35934 const bumpWatchdog2 = () => {
35935 if (dragWatchdog !== null) {
35936 clearTimeout(dragWatchdog);
35937 }
35938 dragWatchdog = setTimeout(resetOverlay, 250);
35939 };
35940 const onDragEnter = (ev) => {
35941 if (!dragHasFiles(ev)) {
35942 return;
35943 }
35944 ev.preventDefault();
35945 dragDepth++;
35946 overlayEl.classList.add("is-active");
35947 bumpWatchdog2();
35948 };
35949 const onDragOver = (ev) => {
35950 if (!dragHasFiles(ev)) {
35951 return;
35952 }
35953 if (ev.defaultPrevented) {
35954 resetOverlay();
35955 return;
35956 }
35957 ev.preventDefault();
35958 if (ev.dataTransfer) {
35959 ev.dataTransfer.dropEffect = "copy";
35960 }
35961 bumpWatchdog2();
35962 };
35963 const onDragLeave = () => {
35964 dragDepth = Math.max(0, dragDepth - 1);
35965 if (dragDepth === 0) {
35966 overlayEl.classList.remove("is-active");
35967 }
35968 };
35969 const onDrop = (ev) => {
35970 if (!dragHasFiles(ev)) {
35971 return;
35972 }
35973 if (ev.defaultPrevented) {
35974 resetOverlay();
35975 return;
35976 }
35977 ev.preventDefault();
35978 resetOverlay();
35979 const entries = snapshotEntries(ev.dataTransfer?.items);
35980 const ctx = classifyDropTarget(ev);
35981 if (entries.some((e) => e.isDirectory)) {
35982 void handleTreeDrop(entries, ctx, opts);
35983 return;
35984 }
35985 const files = ev.dataTransfer?.files ? Array.from(ev.dataTransfer.files) : [];
35986 if (files.length === 0) {
35987 return;
35988 }
35989 void handleFiles(files, ctx, opts);
35990 };
35991 const onDragEnd2 = () => resetOverlay();
35992 const onVisibilityChange = () => {
35993 if (document.visibilityState === "hidden") {
35994 resetOverlay();
35995 }
35996 };
35997 const onIframeMessage = (ev) => {
35998 if (ev.origin !== window.location.origin) {
35999 return;
36000 }
36001 const data = ev.data;
36002 if (!data || data.type !== "desktop-mode-os-file-drop") {
36003 return;
36004 }
36005 if (!Array.isArray(data.files) || data.files.length === 0) {
36006 return;
36007 }
36008 const files = data.files.filter((f) => f instanceof File);
36009 if (files.length === 0) {
36010 return;
36011 }
36012 const windowId = resolveWindowIdFromSource(ev.source);
36013 if (!windowId) {
36014 return;
36015 }
36016 const ctx = {
36017 surface: "iframe",
36018 windowId,
36019 x: typeof data.x === "number" ? data.x : 0,
36020 y: typeof data.y === "number" ? data.y : 0
36021 };
36022 dragDepth = 0;
36023 overlayEl.classList.remove("is-active");
36024 void handleFiles(files, ctx, opts);
36025 };
36026 window.addEventListener("dragenter", onDragEnter);
36027 window.addEventListener("dragover", onDragOver);
36028 window.addEventListener("dragleave", onDragLeave);
36029 window.addEventListener("drop", onDrop);
36030 window.addEventListener("dragend", onDragEnd2);
36031 document.addEventListener("visibilitychange", onVisibilityChange);
36032 window.addEventListener("blur", onDragEnd2);
36033 window.addEventListener("message", onIframeMessage);
36034 const manager2 = {
36035 dispose: () => {
36036 window.removeEventListener("dragenter", onDragEnter);
36037 window.removeEventListener("dragover", onDragOver);
36038 window.removeEventListener("dragleave", onDragLeave);
36039 window.removeEventListener("drop", onDrop);
36040 window.removeEventListener("dragend", onDragEnd2);
36041 document.removeEventListener(
36042 "visibilitychange",
36043 onVisibilityChange
36044 );
36045 window.removeEventListener("blur", onDragEnd2);
36046 window.removeEventListener("message", onIframeMessage);
36047 overlayEl.remove();
36048 delete window.__desktopModeOsFileDropMounted;
36049 }
36050 };
36051 host.__desktopModeOsFileDropMounted = manager2;
36052 return manager2;
36053 }
36054 function ensureDropOverlay() {
36055 const existing = document.querySelector(".desktop-mode-os-drop-overlay");
36056 if (existing) {
36057 return existing;
36058 }
36059 const el = document.createElement("div");
36060 el.className = "desktop-mode-os-drop-overlay";
36061 el.setAttribute("aria-hidden", "true");
36062 el.style.cssText = [
36063 "position:fixed",
36064 "inset:0",
36065 "pointer-events:none",
36066 "z-index:200",
36067 "opacity:0",
36068 "transition:opacity 120ms ease",
36069 "background:radial-gradient(circle at center, rgba(34,113,177,0.18) 0%, rgba(34,113,177,0.06) 60%, transparent 100%)",
36070 "box-shadow:inset 0 0 0 3px rgba(34,113,177,0.55)"
36071 ].join(";");
36072 const label = document.createElement("div");
36073 label.style.cssText = [
36074 "position:absolute",
36075 "top:50%",
36076 "left:50%",
36077 "transform:translate(-50%,-50%)",
36078 "padding:14px 22px",
36079 "border-radius:12px",
36080 "background:rgba(20,20,24,0.78)",
36081 "color:#fff",
36082 "font:600 14px/1.2 -apple-system,BlinkMacSystemFont,sans-serif",
36083 "letter-spacing:0.02em"
36084 ].join(";");
36085 label.textContent = "Drop to upload";
36086 el.appendChild(label);
36087 document.body.appendChild(el);
36088 const style = document.createElement("style");
36089 style.textContent = ".desktop-mode-os-drop-overlay.is-active{opacity:1!important;}";
36090 document.head.appendChild(style);
36091 return el;
36092 }
36093 function mountNoOp() {
36094 const cancel = (ev) => {
36095 if (!dragHasFiles(ev)) {
36096 return;
36097 }
36098 const target2 = ev.target;
36099 if (target2?.closest && IFRAME_PASSTHROUGH_SELECTORS.some((s) => target2.closest(s))) {
36100 return;
36101 }
36102 ev.preventDefault();
36103 };
36104 window.addEventListener("dragover", cancel);
36105 window.addEventListener("drop", cancel);
36106 const host = window;
36107 const manager2 = {
36108 dispose: () => {
36109 window.removeEventListener("dragover", cancel);
36110 window.removeEventListener("drop", cancel);
36111 delete host.__desktopModeOsFileDropMounted;
36112 }
36113 };
36114 host.__desktopModeOsFileDropMounted = manager2;
36115 return manager2;
36116 }
36117 function classifyDropTarget(ev) {
36118 const x = ev.clientX;
36119 const y = ev.clientY;
36120 let node = ev.target;
36121 while (node && node !== document.body) {
36122 if (node.classList.contains("desktop-mode-file-tile") && node.dataset.fileType === "folder") {
36123 const tileRef = Number(
36124 node.dataset.fileRef ?? 0
36125 );
36126 if (Number.isFinite(tileRef) && tileRef > 0) {
36127 return { surface: "folder", folderId: tileRef, x, y };
36128 }
36129 }
36130 if (node.tagName === "IFRAME") {
36131 return {
36132 surface: "iframe",
36133 windowId: windowIdFromElement(node),
36134 x,
36135 y
36136 };
36137 }
36138 if (node.classList.contains("desktop-mode-window") || node.hasAttribute("data-window-id")) {
36139 const windowId = windowIdFromElement(node) ?? (node.getAttribute("data-window-id") || void 0);
36140 const folderMatch = windowId ? /^desktop-mode-folder-(\d+)/.exec(windowId) : null;
36141 if (folderMatch) {
36142 return {
36143 surface: "folder",
36144 folderId: Number(folderMatch[1]),
36145 windowId,
36146 x,
36147 y
36148 };
36149 }
36150 return { surface: "window", windowId, x, y };
36151 }
36152 if (node.dataset?.folderId !== void 0) {
36153 const folderId = Number(
36154 node.dataset.folderId
36155 );
36156 if (Number.isFinite(folderId) && folderId > 0) {
36157 return { surface: "folder", folderId, x, y };
36158 }
36159 return { surface: "wallpaper", x, y };
36160 }
36161 if (node.id === "desktop-mode-wallpaper" || node.classList.contains("desktop-mode-wallpaper") || node.classList.contains("desktop-mode-desktop")) {
36162 return { surface: "wallpaper", x, y };
36163 }
36164 node = node.parentElement;
36165 }
36166 return { surface: "unknown", x, y };
36167 }
36168 async function handleFiles(rawFiles, ctx, opts) {
36169 const detected = applyFilters(
36170 FILE_DROP_HOOKS.FILES_DETECTED,
36171 rawFiles,
36172 ctx
36173 );
36174 if (!Array.isArray(detected) || detected.length === 0) {
36175 return;
36176 }
36177 const { accepted, rejected } = partitionByPolicy(
36178 detected,
36179 opts.config
36180 );
36181 if (rejected.length > 0) {
36182 doAction(FILE_DROP_HOOKS.FILES_REJECTED, {
36183 rejections: rejected,
36184 context: ctx
36185 });
36186 showToast({
36187 message: rejected.length === 1 ? rejected[0].message : `${rejected.length} files couldn't be uploaded.`
36188 });
36189 }
36190 if (accepted.length === 0) {
36191 return;
36192 }
36193 const entries = accepted.map(({ file, mime }) => {
36194 const base = {
36195 file,
36196 mime,
36197 fields: defaultFields(file, mime)
36198 };
36199 const filtered = applyFilters(
36200 FILE_DROP_HOOKS.DIALOG_FIELDS,
36201 base,
36202 ctx
36203 );
36204 if (!filtered || typeof filtered !== "object" || !("fields" in filtered) || typeof filtered.fields !== "object") {
36205 return base;
36206 }
36207 return filtered;
36208 });
36209 await opts.openDialog(entries, ctx);
36210 }
36211 async function handleTreeDrop(entries, ctx, opts) {
36212 if (!opts.storage?.canUpload || !opts.filesUrl) {
36213 showToast({
36214 message: "Folder uploads need desktop storage, which is not available for your account."
36215 });
36216 return;
36217 }
36218 const tree = await collectDroppedTree(entries);
36219 const detected = applyFilters(
36220 FILE_DROP_HOOKS.FILES_DETECTED,
36221 tree.files.map((t) => t.file),
36222 ctx
36223 );
36224 if (!Array.isArray(detected)) {
36225 return;
36226 }
36227 const detectedSet = new Set(detected);
36228 const kept = tree.files.filter((t) => detectedSet.has(t.file));
36229 if (kept.length === 0 && tree.emptyDirs.length === 0) {
36230 return;
36231 }
36232 const { accepted, rejected } = partitionByPolicy(
36233 kept.map((t) => t.file),
36234 opts.config
36235 );
36236 if (rejected.length > 0) {
36237 doAction(FILE_DROP_HOOKS.FILES_REJECTED, {
36238 rejections: rejected,
36239 context: ctx
36240 });
36241 showToast({
36242 message: rejected.length === 1 ? rejected[0].message : `${rejected.length} files couldn't be uploaded.`
36243 });
36244 }
36245 const relByFile = new Map(kept.map((t) => [t.file, t.relativePath]));
36246 const entriesForDialog = accepted.map(({ file, mime }) => ({
36247 file,
36248 mime,
36249 fields: defaultFields(file, mime),
36250 relativePath: relByFile.get(file) ?? ""
36251 }));
36252 if (entriesForDialog.length === 0 && tree.emptyDirs.length === 0) {
36253 return;
36254 }
36255 await opts.openDialog(entriesForDialog, ctx, {
36256 forceDesktop: true,
36257 emptyDirs: tree.emptyDirs
36258 });
36259 }
36260 function partitionByPolicy(files, config) {
36261 const accepted = [];
36262 const rejected = [];
36263 for (const file of files) {
36264 if (file.size === 0) {
36265 rejected.push({
36266 file,
36267 reason: "empty",
36268 message: `“${file.name}” is empty.`
36269 });
36270 continue;
36271 }
36272 if (config.maxSize > 0 && file.size > config.maxSize) {
36273 rejected.push({
36274 file,
36275 reason: "size",
36276 message: `“${file.name}” exceeds the ${formatBytes(
36277 config.maxSize
36278 )} upload limit.`
36279 });
36280 continue;
36281 }
36282 const mime = resolveAllowedMime(
36283 file,
36284 config.allowedMimes,
36285 config.extToMime
36286 );
36287 if (!mime) {
36288 rejected.push({
36289 file,
36290 reason: "mime",
36291 message: `“${file.name}” is not an allowed file type.`
36292 });
36293 continue;
36294 }
36295 accepted.push({ file, mime });
36296 }
36297 return { accepted, rejected };
36298 }
36299 function resolveAllowedMime(file, allowedMimes, extToMime) {
36300 if (allowedMimes.length === 0) {
36301 return null;
36302 }
36303 const lower = file.type.toLowerCase();
36304 if (lower && allowedMimes.includes(lower)) {
36305 return lower;
36306 }
36307 const ext = extensionOf(file.name);
36308 if (!ext) {
36309 return null;
36310 }
36311 if (extToMime) {
36312 for (const [key, mime] of Object.entries(extToMime)) {
36313 if (key.split("|").includes(ext) && allowedMimes.includes(mime)) {
36314 return mime;
36315 }
36316 }
36317 return null;
36318 }
36319 const guess = EXTENSION_GUESSES[ext];
36320 if (guess && allowedMimes.includes(guess)) {
36321 return guess;
36322 }
36323 return null;
36324 }
36325 const EXTENSION_GUESSES = {
36326 jpg: "image/jpeg",
36327 jpeg: "image/jpeg",
36328 png: "image/png",
36329 gif: "image/gif",
36330 webp: "image/webp",
36331 avif: "image/avif",
36332 heic: "image/heic",
36333 heif: "image/heif",
36334 svg: "image/svg+xml",
36335 mp4: "video/mp4",
36336 mov: "video/quicktime",
36337 webm: "video/webm",
36338 mp3: "audio/mpeg",
36339 wav: "audio/wav",
36340 pdf: "application/pdf"
36341 };
36342 function extensionOf(name) {
36343 const dot = name.lastIndexOf(".");
36344 if (dot < 0) {
36345 return "";
36346 }
36347 return name.slice(dot + 1).toLowerCase();
36348 }
36349 function defaultFields(file, mime) {
36350 const safeName = sanitizeFilename(file.name);
36351 const ext = extensionOf(safeName);
36352 const stem = ext ? safeName.slice(0, safeName.length - ext.length - 1) : safeName;
36353 const title = humanize(stem);
36354 return {
36355 title,
36356 altText: mime.startsWith("image/") ? title : "",
36357 caption: "",
36358 description: "",
36359 filename: safeName
36360 };
36361 }
36362 function sanitizeFilename(name) {
36363 const cleaned = name.replace(/[\\/]/g, "-").replace(/[\x00-\x1f\x7f]/g, "").replace(/\s+/g, " ").replace(/ *- */g, "-").replace(/-+/g, "-").trim().replace(/^[-.]+|[-.]+$/g, "");
36364 return cleaned || "upload";
36365 }
36366 function humanize(stem) {
36367 const spaced = stem.replace(/[-_]+/g, " ").trim();
36368 if (!spaced) {
36369 return "Upload";
36370 }
36371 return spaced.charAt(0).toUpperCase() + spaced.slice(1);
36372 }
36373 function formatBytes(bytes) {
36374 if (bytes >= 1024 * 1024) {
36375 return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
36376 }
36377 if (bytes >= 1024) {
36378 return `${(bytes / 1024).toFixed(0)} KB`;
36379 }
36380 return `${bytes} B`;
36381 }
36382 const manager = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
36383 __proto__: null,
36384 IFRAME_PASSTHROUGH_SELECTORS,
36385 classifyDropTarget,
36386 defaultFields,
36387 dragHasFiles,
36388 handleFiles,
36389 handleTreeDrop,
36390 humanize,
36391 mountOsFileDropManager,
36392 partitionByPolicy,
36393 resolveAllowedMime,
36394 sanitizeFilename,
36395 windowIdFromElement
36396 }, Symbol.toStringTag, { value: "Module" }));
36397 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}`;
36398 const _WpdTextField = class _WpdTextField extends Component {
36399 constructor() {
36400 super(...arguments);
36401 this._revealed = false;
36402 }
36403 connectedCallback() {
36404 super.connectedCallback();
36405 ensureAutoId(this);
36406 }
36407 render() {
36408 const label = this.label || "";
36409 const value = this.value ?? "";
36410 const placeholder = this.placeholder || "";
36411 const disabled = this.disabled !== null;
36412 const readonly = this.readonly !== null;
36413 const declaredAutocomplete = this.autocomplete;
36414 const declaredType = this.type || "text";
36415 const isPassword = declaredType === "password";
36416 let autocomplete = declaredAutocomplete || "off";
36417 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
36418 autocomplete = "new-password";
36419 }
36420 const maxLength = this.maxlength;
36421 const minLength = this.minlength;
36422 const pattern = this.pattern || "";
36423 const name = this.name || "";
36424 const suffix = this.suffix || "";
36425 const invalid = this.invalid !== null;
36426 const reveal = this.reveal !== null;
36427 const isPasswordIntent = declaredType === "password";
36428 const isMasked = isPasswordIntent && !(reveal && this._revealed);
36429 let effectiveType;
36430 if (isPasswordIntent) {
36431 effectiveType = "text";
36432 } else if (reveal && this._revealed) {
36433 effectiveType = "text";
36434 } else {
36435 effectiveType = declaredType;
36436 }
36437 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
36438 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
36439 const hostId = this.id || "wpd-unnamed";
36440 const inputId = `${hostId}__input`;
36441 return html`
36442 ${label ? html`<label
36443 class="wpd-text-field__label"
36444 for=${inputId}
36445 >${label}</label>` : html``}
36446 <span class=${rowClass}>
36447 <input
36448 id=${inputId}
36449 class=${inputClass}
36450 type=${effectiveType}
36451 .value=${value}
36452 placeholder=${placeholder}
36453 ?disabled=${disabled}
36454 ?readonly=${readonly}
36455 autocomplete=${autocomplete}
36456 maxlength=${maxLength ?? ""}
36457 minlength=${minLength ?? ""}
36458 pattern=${pattern}
36459 name=${name}
36460 aria-invalid=${invalid ? "true" : "false"}
36461 aria-label=${label || ""}
36462 @input=${(e) => this._onInput(e)}
36463 @change=${(e) => this._onChange(e)}
36464 @keydown=${(e) => this._onKeyDown(e)}
36465 />
36466 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
36467 ${reveal ? this._renderRevealButton(disabled) : html``}
36468 </span>
36469 `;
36470 }
36471 _renderRevealButton(disabled) {
36472 const label = this._revealed ? "Hide" : "Show";
36473 return html`
36474 <button
36475 type="button"
36476 class="wpd-text-field__reveal"
36477 aria-label=${label}
36478 aria-pressed=${this._revealed ? "true" : "false"}
36479 ?disabled=${disabled}
36480 tabindex="0"
36481 @click=${() => this._onToggleReveal()}
36482 >
36483 ${this._revealed ? _iconEyeOff() : _iconEye()}
36484 </button>
36485 `;
36486 }
36487 _onToggleReveal() {
36488 this._revealed = !this._revealed;
36489 this.requestUpdate();
36490 }
36491 _onInput(e) {
36492 const input = e.target;
36493 this.value = input.value;
36494 this.emit("wpd-input-change", { value: input.value });
36495 }
36496 _onChange(e) {
36497 const input = e.target;
36498 this.emit("wpd-input-commit", { value: input.value });
36499 }
36500 _onKeyDown(e) {
36501 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
36502 const input = e.target;
36503 this.emit("wpd-submit", { value: input.value });
36504 }
36505 }
36506 };
36507 _WpdTextField.props = [
36508 "label",
36509 "value",
36510 "placeholder",
36511 "disabled",
36512 "readonly",
36513 "autocomplete",
36514 "type",
36515 "maxlength",
36516 "minlength",
36517 "pattern",
36518 "name",
36519 "suffix",
36520 "invalid",
36521 "reveal"
36522 ];
36523 _WpdTextField.styles = [textFieldStyles];
36524 _WpdTextField.help = {
36525 title: "Text field",
36526 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.",
36527 status: "stable",
36528 since: "0.5.0",
36529 props: [
36530 { name: "label", type: "string", description: "Visible label above the input." },
36531 { name: "value", type: "string", description: "Current input value; reflected two-way." },
36532 { name: "placeholder", type: "string", description: "Native placeholder string." },
36533 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
36534 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
36535 {
36536 name: "autocomplete",
36537 type: "string",
36538 default: "off",
36539 description: "Forwarded to the native input autocomplete attribute."
36540 },
36541 {
36542 name: "type",
36543 type: "string",
36544 default: "text",
36545 description: "Native input type (text, password, email, search, tel, url)."
36546 },
36547 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
36548 { name: "minlength", type: "integer (string)", description: "Native minlength." },
36549 { name: "pattern", type: "regex string", description: "Native validation pattern." },
36550 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
36551 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
36552 {
36553 name: "invalid",
36554 type: "boolean attribute",
36555 description: "Marks the field aria-invalid and applies the error style."
36556 },
36557 {
36558 name: "reveal",
36559 type: "boolean attribute",
36560 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
36561 }
36562 ],
36563 events: [
36564 {
36565 name: "wpd-input-change",
36566 description: "Fires on every input keystroke.",
36567 detail: "{ value: string }"
36568 },
36569 {
36570 name: "wpd-input-commit",
36571 description: "Fires on the native change event (blur / Enter).",
36572 detail: "{ value: string }"
36573 },
36574 {
36575 name: "wpd-submit",
36576 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
36577 detail: "{ value: string }"
36578 }
36579 ],
36580 cssProps: [
36581 { name: "--desktop-mode-text", description: "Text colour." },
36582 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
36583 { name: "--desktop-mode-border", description: "Input outline." },
36584 { name: "--desktop-mode-window-bg", description: "Input background." }
36585 ],
36586 example: html`
36587 <wpd-stack gap="8">
36588 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
36589 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
36590 </wpd-stack>
36591 `
36592 };
36593 let WpdTextField = _WpdTextField;
36594 defineComponent("wpd-text-field", WpdTextField);
36595 function _iconEye() {
36596 return html`
36597 <svg
36598 viewBox="0 0 16 16"
36599 width="14"
36600 height="14"
36601 fill="none"
36602 stroke="currentColor"
36603 stroke-width="1.5"
36604 stroke-linecap="round"
36605 stroke-linejoin="round"
36606 aria-hidden="true"
36607 focusable="false"
36608 >
36609 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
36610 <circle cx="8" cy="8" r="2" />
36611 </svg>
36612 `;
36613 }
36614 function _iconEyeOff() {
36615 return html`
36616 <svg
36617 viewBox="0 0 16 16"
36618 width="14"
36619 height="14"
36620 fill="none"
36621 stroke="currentColor"
36622 stroke-width="1.5"
36623 stroke-linecap="round"
36624 stroke-linejoin="round"
36625 aria-hidden="true"
36626 focusable="false"
36627 >
36628 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
36629 <circle cx="8" cy="8" r="2" />
36630 <line x1="2" y1="2" x2="14" y2="14" />
36631 </svg>
36632 `;
36633 }
36634 async function uploadFile(args) {
36635 const initial = {
36636 file: args.file,
36637 mime: args.mime,
36638 fields: args.fields
36639 };
36640 const filtered = applyFilters(
36641 FILE_DROP_HOOKS.BEFORE_UPLOAD,
36642 initial,
36643 args.context
36644 );
36645 if (!filtered) {
36646 throw new UploadCancelledError();
36647 }
36648 const body = new FormData();
36649 const renamed = filtered.fields.filename !== filtered.file.name ? new File([filtered.file], filtered.fields.filename, {
36650 type: filtered.mime || filtered.file.type
36651 }) : filtered.file;
36652 body.append("file", renamed);
36653 body.append("title", filtered.fields.title);
36654 body.append("alt_text", filtered.fields.altText);
36655 body.append("caption", filtered.fields.caption);
36656 body.append("description", filtered.fields.description);
36657 return new Promise((resolve2, reject) => {
36658 const xhr = new XMLHttpRequest();
36659 xhr.open("POST", args.mediaUrl, true);
36660 xhr.withCredentials = true;
36661 xhr.setRequestHeader("X-WP-Nonce", args.restNonce);
36662 xhr.responseType = "text";
36663 let aborted = false;
36664 let bodyFullySent = false;
36665 let cancelRequested = false;
36666 const abort = () => {
36667 cancelRequested = true;
36668 if (bodyFullySent) {
36669 return;
36670 }
36671 aborted = true;
36672 try {
36673 xhr.abort();
36674 } catch {
36675 }
36676 };
36677 doAction(FILE_DROP_HOOKS.UPLOAD_STARTED, {
36678 file: filtered.file,
36679 fields: filtered.fields,
36680 context: args.context,
36681 abort
36682 });
36683 xhr.upload.addEventListener("progress", (e) => {
36684 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
36685 file: filtered.file,
36686 fields: filtered.fields,
36687 context: args.context,
36688 loaded: e.loaded,
36689 total: e.lengthComputable ? e.total : 0,
36690 indeterminate: !e.lengthComputable
36691 });
36692 });
36693 xhr.upload.addEventListener("load", () => {
36694 bodyFullySent = true;
36695 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
36696 file: filtered.file,
36697 fields: filtered.fields,
36698 context: args.context,
36699 loaded: filtered.file.size,
36700 total: filtered.file.size,
36701 indeterminate: false
36702 });
36703 });
36704 xhr.addEventListener("error", () => {
36705 if (aborted) {
36706 return;
36707 }
36708 const error = new Error("Network error during upload.");
36709 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
36710 // `filtered.file` — same identity as UPLOAD_STARTED /
36711 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
36712 // that swapped the File would otherwise route this
36713 // failure to a row keyed by the original (pre-swap)
36714 // File, leaving the HUD row stuck in "running".
36715 file: filtered.file,
36716 error,
36717 context: args.context
36718 });
36719 reject(error);
36720 });
36721 xhr.addEventListener("abort", () => {
36722 const error = new UploadAbortedError();
36723 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
36724 // `filtered.file` — same identity as UPLOAD_STARTED /
36725 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
36726 // that swapped the File would otherwise route this
36727 // failure to a row keyed by the original (pre-swap)
36728 // File, leaving the HUD row stuck in "running".
36729 file: filtered.file,
36730 error,
36731 context: args.context
36732 });
36733 reject(error);
36734 });
36735 xhr.addEventListener("load", () => {
36736 if (aborted) {
36737 return;
36738 }
36739 if (xhr.status < 200 || xhr.status >= 300) {
36740 const message = extractXhrMessage(xhr);
36741 const error = new Error(message);
36742 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
36743 file: filtered.file,
36744 error,
36745 context: args.context
36746 });
36747 reject(error);
36748 return;
36749 }
36750 let data;
36751 try {
36752 data = JSON.parse(xhr.responseText);
36753 } catch (err) {
36754 const error = err instanceof Error ? err : new Error("Could not parse server response.");
36755 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
36756 file: filtered.file,
36757 error,
36758 context: args.context
36759 });
36760 reject(error);
36761 return;
36762 }
36763 if (cancelRequested && data.id) {
36764 void deleteAttachment(
36765 args.mediaUrl,
36766 args.restNonce,
36767 data.id
36768 );
36769 const error = new UploadAbortedError();
36770 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
36771 file: filtered.file,
36772 error,
36773 context: args.context
36774 });
36775 reject(error);
36776 return;
36777 }
36778 const result = {
36779 id: data.id,
36780 url: data.source_url,
36781 mime: data.mime_type || filtered.mime,
36782 title: data.title?.rendered || filtered.fields.title,
36783 filename: data.media_details?.file || filtered.fields.filename
36784 };
36785 doAction(FILE_DROP_HOOKS.AFTER_UPLOAD, {
36786 file: filtered.file,
36787 result,
36788 fields: filtered.fields,
36789 context: args.context
36790 });
36791 resolve2(result);
36792 });
36793 xhr.send(body);
36794 });
36795 }
36796 class UploadCancelledError extends Error {
36797 constructor() {
36798 super("Upload cancelled by desktop-mode.drop.before-upload filter.");
36799 this.name = "UploadCancelledError";
36800 }
36801 }
36802 class UploadAbortedError extends Error {
36803 constructor() {
36804 super("Upload aborted by the caller.");
36805 this.name = "UploadAbortedError";
36806 }
36807 }
36808 function deleteAttachment(mediaUrl, restNonce, id) {
36809 const url = `${mediaUrl.replace(/\/$/, "")}/${id}?force=true`;
36810 const cleanup = new XMLHttpRequest();
36811 cleanup.open("DELETE", url, true);
36812 cleanup.withCredentials = true;
36813 cleanup.setRequestHeader("X-WP-Nonce", restNonce);
36814 return new Promise((resolve2) => {
36815 cleanup.addEventListener("loadend", () => {
36816 if (cleanup.status < 200 || cleanup.status >= 300) {
36817 console.warn(
36818 `[os-file-drop] late-cancel cleanup failed for attachment ${id} (HTTP ${cleanup.status}). The attachment remains in the Media Library; delete it manually.`
36819 );
36820 }
36821 resolve2();
36822 });
36823 cleanup.addEventListener("error", () => {
36824 console.warn(
36825 `[os-file-drop] late-cancel cleanup network error for attachment ${id}. The attachment remains in the Media Library; delete it manually.`
36826 );
36827 resolve2();
36828 });
36829 try {
36830 cleanup.send();
36831 } catch (err) {
36832 console.warn(
36833 `[os-file-drop] late-cancel cleanup could not be dispatched for attachment ${id}:`,
36834 err
36835 );
36836 resolve2();
36837 }
36838 });
36839 }
36840 function extractXhrMessage(xhr) {
36841 const fallback = `Upload failed (HTTP ${xhr.status}).`;
36842 const text = xhr.responseText;
36843 if (!text) {
36844 return fallback;
36845 }
36846 try {
36847 const data = JSON.parse(text);
36848 if (data && typeof data.message === "string") {
36849 return data.message;
36850 }
36851 } catch {
36852 }
36853 return fallback;
36854 }
36855 async function uploadFileToDesktop(args) {
36856 const initial = {
36857 file: args.file,
36858 mime: args.mime,
36859 fields: args.fields
36860 };
36861 const filtered = applyFilters(
36862 FILE_DROP_HOOKS.BEFORE_UPLOAD,
36863 initial,
36864 args.context
36865 );
36866 if (!filtered) {
36867 throw new UploadCancelledError();
36868 }
36869 const body = new FormData();
36870 const renamed = filtered.fields.filename !== filtered.file.name ? new File([filtered.file], filtered.fields.filename, {
36871 type: filtered.mime || filtered.file.type
36872 }) : filtered.file;
36873 body.append("file", renamed);
36874 body.append("parentId", String(args.parentId));
36875 if (args.relativePath) {
36876 body.append("relativePath", args.relativePath);
36877 }
36878 if (args.coords) {
36879 body.append("x", String(args.coords.x));
36880 body.append("y", String(args.coords.y));
36881 }
36882 const url = `${args.filesUrl.replace(/\/$/, "")}/uploads`;
36883 return new Promise((resolve2, reject) => {
36884 const xhr = new XMLHttpRequest();
36885 xhr.open("POST", url, true);
36886 xhr.withCredentials = true;
36887 xhr.setRequestHeader("X-WP-Nonce", args.restNonce);
36888 xhr.responseType = "text";
36889 let aborted = false;
36890 const abort = () => {
36891 aborted = true;
36892 try {
36893 xhr.abort();
36894 } catch {
36895 }
36896 };
36897 doAction(FILE_DROP_HOOKS.UPLOAD_STARTED, {
36898 file: filtered.file,
36899 fields: filtered.fields,
36900 context: args.context,
36901 abort
36902 });
36903 xhr.upload.addEventListener("progress", (e) => {
36904 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
36905 file: filtered.file,
36906 fields: filtered.fields,
36907 context: args.context,
36908 loaded: e.loaded,
36909 total: e.lengthComputable ? e.total : 0,
36910 indeterminate: !e.lengthComputable
36911 });
36912 });
36913 const fail = (error) => {
36914 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
36915 file: filtered.file,
36916 error,
36917 context: args.context
36918 });
36919 reject(error);
36920 };
36921 xhr.addEventListener("error", () => {
36922 if (!aborted) {
36923 fail(new Error("Network error during upload."));
36924 }
36925 });
36926 xhr.addEventListener("abort", () => fail(new UploadAbortedError()));
36927 xhr.addEventListener("load", () => {
36928 if (aborted) {
36929 return;
36930 }
36931 if (xhr.status < 200 || xhr.status >= 300) {
36932 fail(new Error(extractMessage(xhr, filtered.file.name)));
36933 return;
36934 }
36935 let data;
36936 try {
36937 data = JSON.parse(xhr.responseText);
36938 } catch {
36939 fail(new Error("Could not parse server response."));
36940 return;
36941 }
36942 if (!data.placement || typeof data.storedFileId !== "number") {
36943 fail(new Error("Unexpected server response."));
36944 return;
36945 }
36946 upsertPlacement(data.placement, "local");
36947 const result = {
36948 placement: data.placement,
36949 storedFileId: data.storedFileId
36950 };
36951 doAction(FILE_DROP_HOOKS.AFTER_UPLOAD, {
36952 file: filtered.file,
36953 result,
36954 fields: filtered.fields,
36955 context: args.context
36956 });
36957 resolve2(result);
36958 });
36959 xhr.send(body);
36960 });
36961 }
36962 function extractMessage(xhr, fileName) {
36963 if (xhr.status === 413) {
36964 return `“${fileName}” is larger than this server accepts.`;
36965 }
36966 const fallback = `Upload failed (HTTP ${xhr.status}).`;
36967 const text = xhr.responseText;
36968 if (!text) {
36969 return fallback;
36970 }
36971 try {
36972 const data = JSON.parse(text);
36973 if (data && typeof data.message === "string" && data.message) {
36974 return data.message;
36975 }
36976 } catch {
36977 }
36978 return fallback;
36979 }
36980 function snapToGrid(x, y) {
36981 const col = Math.max(0, Math.round((x - 16) / 96));
36982 const row = Math.max(0, Math.round((y - 16) / 110));
36983 return { x: 16 + col * 96, y: 16 + row * 110 };
36984 }
36985 const MEDIA_KIND_RE = /^(image|video|audio)\//;
36986 function resolveDefaultDestination(opts) {
36987 if (!opts.desktopAllowed) {
36988 return "media";
36989 }
36990 if (opts.forceDesktop || opts.preferDesktop) {
36991 return "desktop";
36992 }
36993 if (opts.surface === "window" || opts.surface === "iframe") {
36994 return "media";
36995 }
36996 if ((opts.folderId ?? 0) > 0) {
36997 return "desktop";
36998 }
36999 const allMedia = opts.mimes.length > 0 && opts.mimes.every((m) => MEDIA_KIND_RE.test(m));
37000 return allMedia ? "media" : "desktop";
37001 }
37002 let activeDialog = null;
37003 async function openUploadDialog(args) {
37004 if (args.entries.length === 0 && !args.emptyDirs?.length) {
37005 return;
37006 }
37007 if (activeDialog) {
37008 activeDialog.replace(args);
37009 return;
37010 }
37011 const desktopAllowed = !!(args.storage?.canUpload && args.filesUrl);
37012 let destination = resolveDefaultDestination({
37013 desktopAllowed,
37014 surface: args.context.surface,
37015 folderId: args.context.folderId,
37016 forceDesktop: args.forceDesktop,
37017 preferDesktop: args.preferDesktop,
37018 mimes: args.entries.map((e) => e.mime)
37019 });
37020 const modal = document.createElement("wpd-modal");
37021 modal.setAttribute("open", "");
37022 modal.setAttribute("size", "md");
37023 document.body.appendChild(modal);
37024 const syncTitle = () => {
37025 const count = args.entries.length;
37026 let target2 = "Media Library";
37027 if (destination === "desktop") {
37028 target2 = (args.context.folderId ?? 0) > 0 ? "this folder" : "Desktop";
37029 }
37030 let title = `Upload ${count} files to ${target2}`;
37031 if (count === 0) {
37032 title = (args.context.folderId ?? 0) > 0 ? "Create folders in this folder" : "Create folders on Desktop";
37033 } else if (count === 1) {
37034 title = `Upload to ${target2}`;
37035 }
37036 modal.setAttribute("title", title);
37037 };
37038 syncTitle();
37039 const draft = args.entries.map((entry) => ({
37040 ...entry.fields
37041 }));
37042 const renderBody = () => {
37043 modal.innerHTML = "";
37044 if (desktopAllowed && !args.forceDesktop) {
37045 const destWrap = document.createElement("div");
37046 destWrap.style.cssText = "display:flex;align-items:center;gap:10px;margin-bottom:14px;";
37047 const destLabel = document.createElement("span");
37048 destLabel.textContent = "Upload to";
37049 destLabel.style.cssText = "font-weight:600;";
37050 destWrap.appendChild(destLabel);
37051 const segmented = document.createElement("wpd-segmented");
37052 segmented.setAttribute("value", destination);
37053 segmented.setAttribute("label", "Destination");
37054 segmented.style.setProperty("--wpd-segmented-bg", "rgba(255,255,255,0.06)");
37055 const segDesktop = document.createElement("wpd-segment");
37056 segDesktop.setAttribute("value", "desktop");
37057 segDesktop.textContent = "Desktop";
37058 segmented.appendChild(segDesktop);
37059 const segMedia = document.createElement("wpd-segment");
37060 segMedia.setAttribute("value", "media");
37061 segMedia.textContent = "Media Library";
37062 segmented.appendChild(segMedia);
37063 segmented.addEventListener("wpd-pick", (e) => {
37064 const detail = e.detail;
37065 destination = detail.value;
37066 syncTitle();
37067 renderBody();
37068 });
37069 destWrap.appendChild(segmented);
37070 modal.appendChild(destWrap);
37071 } else if (args.forceDesktop) {
37072 const note = document.createElement("div");
37073 note.style.cssText = "opacity:0.7;font-size:12px;margin-bottom:14px;";
37074 note.textContent = "Folder uploads land in your desktop storage, preserving the folder structure.";
37075 modal.appendChild(note);
37076 }
37077 const maxBytes = destination === "desktop" ? args.storage?.maxBytes ?? 0 : args.mediaMaxBytes ?? 0;
37078 if (maxBytes > 0) {
37079 const cap = document.createElement("div");
37080 cap.className = "desktop-mode-upload-dialog__max-size";
37081 cap.style.cssText = "opacity:0.6;font-size:12px;margin-bottom:14px;";
37082 cap.textContent = `Maximum file size: ${formatBytes$1(maxBytes)}`;
37083 modal.appendChild(cap);
37084 }
37085 const list2 = document.createElement("div");
37086 list2.style.cssText = "display:flex;flex-direction:column;gap:18px;max-height:60vh;overflow:auto;padding-right:6px;";
37087 args.entries.forEach((entry, i) => {
37088 list2.appendChild(renderEntry(entry, draft[i], i + 1));
37089 });
37090 modal.appendChild(list2);
37091 const footer = document.createElement("div");
37092 footer.setAttribute("slot", "footer");
37093 footer.style.cssText = "display:flex;gap:8px;justify-content:flex-end;";
37094 const cancel = document.createElement("wpd-button");
37095 cancel.setAttribute("variant", "secondary");
37096 cancel.textContent = "Cancel";
37097 cancel.addEventListener("click", () => {
37098 modal.remove();
37099 });
37100 const upload = document.createElement("wpd-button");
37101 upload.setAttribute("variant", "primary");
37102 if (args.entries.length === 0) {
37103 upload.textContent = "Create folders";
37104 } else {
37105 upload.textContent = args.entries.length === 1 ? "Upload" : `Upload ${args.entries.length} files`;
37106 }
37107 upload.addEventListener("click", () => {
37108 void runUploads(upload, cancel);
37109 });
37110 footer.appendChild(cancel);
37111 footer.appendChild(upload);
37112 modal.appendChild(footer);
37113 };
37114 const renderEntry = (entry, fields, index2) => {
37115 const wrap = document.createElement("div");
37116 wrap.style.cssText = "display:flex;flex-direction:column;gap:8px;border:1px solid rgba(255,255,255,0.08);border-radius:10px;padding:14px;";
37117 const heading = document.createElement("div");
37118 heading.style.cssText = "display:flex;gap:10px;align-items:center;font-weight:600;";
37119 const tag = document.createElement("span");
37120 tag.textContent = args.entries.length === 1 ? "" : `#${index2} · `;
37121 tag.style.opacity = "0.6";
37122 const fname = document.createElement("span");
37123 fname.textContent = entry.file.name;
37124 fname.style.cssText = "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
37125 const size = document.createElement("span");
37126 size.textContent = `${entry.mime || "unknown"} · ${formatBytes$1(
37127 entry.file.size
37128 )}`;
37129 size.style.cssText = "opacity:0.6;font-size:12px;";
37130 heading.appendChild(tag);
37131 heading.appendChild(fname);
37132 heading.appendChild(size);
37133 wrap.appendChild(heading);
37134 if (destination === "desktop") {
37135 if (entry.relativePath) {
37136 const path = document.createElement("div");
37137 path.textContent = entry.relativePath;
37138 path.style.cssText = "opacity:0.55;font-size:12px;";
37139 wrap.appendChild(path);
37140 }
37141 wrap.appendChild(
37142 textField("Filename", fields.filename, (v) => fields.filename = v)
37143 );
37144 return wrap;
37145 }
37146 wrap.appendChild(textField("Title", fields.title, (v) => fields.title = v));
37147 wrap.appendChild(textField("Filename", fields.filename, (v) => fields.filename = v));
37148 if (entry.mime.startsWith("image/")) {
37149 wrap.appendChild(
37150 textField("Alt text", fields.altText, (v) => fields.altText = v)
37151 );
37152 }
37153 wrap.appendChild(textField("Caption", fields.caption, (v) => fields.caption = v));
37154 wrap.appendChild(
37155 textareaField("Description", fields.description, (v) => fields.description = v)
37156 );
37157 return wrap;
37158 };
37159 const runUploads = async (uploadBtn, cancelBtn) => {
37160 if (activeDialog === handle) {
37161 activeDialog = null;
37162 }
37163 uploadBtn.disabled = true;
37164 cancelBtn.disabled = true;
37165 uploadBtn.textContent = "Uploading…";
37166 const total = args.entries.length;
37167 let successes = 0;
37168 let failures = 0;
37169 let cancelled = 0;
37170 const failureDetails = [];
37171 const parentId = args.context.folderId ?? 0;
37172 let firstFlatPlaced = false;
37173 for (let i = 0; i < total; i++) {
37174 const entry = args.entries[i];
37175 try {
37176 if (destination === "desktop" && args.filesUrl) {
37177 const isFlat = !entry.relativePath;
37178 const coords = isFlat && !firstFlatPlaced && args.context.surface === "wallpaper" ? snapToGrid(args.context.x, args.context.y) : void 0;
37179 if (coords) {
37180 firstFlatPlaced = true;
37181 }
37182 await uploadFileToDesktop({
37183 file: entry.file,
37184 mime: entry.mime,
37185 fields: draft[i],
37186 context: args.context,
37187 filesUrl: args.filesUrl,
37188 restNonce: args.restNonce,
37189 parentId,
37190 relativePath: entry.relativePath ?? "",
37191 coords
37192 });
37193 } else {
37194 await uploadFile({
37195 file: entry.file,
37196 mime: entry.mime,
37197 fields: draft[i],
37198 context: args.context,
37199 mediaUrl: args.mediaUrl,
37200 restNonce: args.restNonce
37201 });
37202 }
37203 successes++;
37204 } catch (err) {
37205 if (err instanceof UploadCancelledError) {
37206 cancelled++;
37207 continue;
37208 }
37209 if (err instanceof UploadAbortedError) {
37210 cancelled++;
37211 continue;
37212 }
37213 failures++;
37214 const message = err instanceof Error ? err.message : "Upload failed.";
37215 failureDetails.push(`“${entry.file.name}” — ${message}`);
37216 }
37217 }
37218 if (destination === "desktop" && args.emptyDirs?.length) {
37219 for (const dir of args.emptyDirs) {
37220 try {
37221 await ensureUploadPath(parentId, dir);
37222 } catch {
37223 }
37224 }
37225 }
37226 modal.remove();
37227 showBatchSummaryToast({
37228 total,
37229 successes,
37230 failures,
37231 cancelled,
37232 failureDetails,
37233 destination
37234 });
37235 };
37236 const handle = {
37237 replace: (next) => {
37238 args.entries = next.entries;
37239 args.emptyDirs = next.emptyDirs;
37240 args.forceDesktop = next.forceDesktop;
37241 args.preferDesktop = next.preferDesktop;
37242 args.context = next.context;
37243 args.mediaMaxBytes = next.mediaMaxBytes ?? args.mediaMaxBytes;
37244 draft.length = 0;
37245 for (const entry of next.entries) {
37246 draft.push({ ...entry.fields });
37247 }
37248 destination = resolveDefaultDestination({
37249 desktopAllowed,
37250 surface: next.context.surface,
37251 folderId: next.context.folderId,
37252 forceDesktop: next.forceDesktop,
37253 preferDesktop: next.preferDesktop,
37254 mimes: next.entries.map((e) => e.mime)
37255 });
37256 syncTitle();
37257 renderBody();
37258 }
37259 };
37260 activeDialog = handle;
37261 renderBody();
37262 await new Promise((resolve2) => {
37263 const finish = () => {
37264 if (activeDialog === handle) {
37265 activeDialog = null;
37266 }
37267 resolve2();
37268 };
37269 modal.addEventListener("wpd-modal-cancel", () => {
37270 modal.remove();
37271 finish();
37272 });
37273 const observer = new MutationObserver(() => {
37274 if (!modal.isConnected) {
37275 observer.disconnect();
37276 finish();
37277 }
37278 });
37279 observer.observe(document.body, { childList: true, subtree: true });
37280 });
37281 }
37282 function textField(label, value, onChange) {
37283 const el = document.createElement("wpd-text-field");
37284 el.setAttribute("label", label);
37285 el.setAttribute("value", value);
37286 el.addEventListener("input", () => {
37287 const v = el.value;
37288 if (typeof v === "string") {
37289 onChange(v);
37290 }
37291 });
37292 return el;
37293 }
37294 function textareaField(label, value, onChange) {
37295 const el = document.createElement("wpd-textarea");
37296 el.setAttribute("label", label);
37297 el.setAttribute("value", value);
37298 el.setAttribute("rows", "3");
37299 el.addEventListener("input", () => {
37300 const v = el.value;
37301 if (typeof v === "string") {
37302 onChange(v);
37303 }
37304 });
37305 return el;
37306 }
37307 function showBatchSummaryToast(args) {
37308 const { total, successes, failures, cancelled, failureDetails } = args;
37309 const target2 = args.destination === "desktop" ? "your desktop" : "Media Library";
37310 if (total === 0) {
37311 if (args.destination === "desktop") {
37312 showToast({ message: "Folder created on your desktop." });
37313 }
37314 return;
37315 }
37316 if (total === 1) {
37317 if (successes === 1) {
37318 showToast({ message: `Uploaded to ${target2}.` });
37319 } else if (failures === 1 && failureDetails[0]) {
37320 showToast({ message: failureDetails[0] });
37321 } else if (cancelled === 1) {
37322 showToast({ message: "Upload cancelled." });
37323 }
37324 return;
37325 }
37326 if (successes === total) {
37327 showToast({
37328 message: `Uploaded ${successes} files to ${target2}.`
37329 });
37330 return;
37331 }
37332 if (cancelled === total) {
37333 showToast({ message: "All uploads cancelled." });
37334 return;
37335 }
37336 if (failures === total) {
37337 showToast({
37338 message: failures === 1 && failureDetails[0] ? failureDetails[0] : `${failures} uploads failed.`
37339 });
37340 return;
37341 }
37342 const parts = [];
37343 if (successes > 0) {
37344 parts.push(
37345 `Uploaded ${successes} file${successes === 1 ? "" : "s"}.`
37346 );
37347 }
37348 if (cancelled > 0) {
37349 parts.push(`Cancelled ${cancelled}.`);
37350 }
37351 if (failures > 0) {
37352 parts.push(`Failed ${failures}.`);
37353 }
37354 showToast({ message: parts.join(" ") });
37355 }
37356 const dialog = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
37357 __proto__: null,
37358 openUploadDialog,
37359 resolveDefaultDestination
37360 }, Symbol.toStringTag, { value: "Module" }));
37361 const _initial = {
37362 tab: null,
37363 requestedAt: 0
37364 };
37365 let _store = null;
37366 function getStore() {
37367 if (_store) {
37368 return _store;
37369 }
37370 const w = window;
37371 const factory = w.wp?.desktop?.createSharedStore;
37372 if (typeof factory !== "function") {
37373 return null;
37374 }
37375 _store = factory(
37376 "desktop-mode/plugins-window/tab-target",
37377 () => ({ ..._initial })
37378 );
37379 return _store;
37380 }
37381 function setPluginsWindowTab(tab) {
37382 const store2 = getStore();
37383 if (store2) {
37384 store2.state.tab = tab;
37385 store2.state.requestedAt = Date.now();
37386 store2.notify();
37387 return;
37388 }
37389 const w = window;
37390 w._wpdPluginsWindowTab = { tab, requestedAt: Date.now() };
37391 }
37392 function consumePluginsWindowTab() {
37393 const store2 = getStore();
37394 if (store2) {
37395 const tab = store2.state.tab;
37396 if (tab !== null) {
37397 store2.state.tab = null;
37398 store2.state.requestedAt = 0;
37399 store2.notify();
37400 }
37401 return tab;
37402 }
37403 const w = window;
37404 const prev = w._wpdPluginsWindowTab;
37405 if (prev) {
37406 w._wpdPluginsWindowTab = { tab: null, requestedAt: 0 };
37407 return prev.tab;
37408 }
37409 return null;
37410 }
37411 function subscribePluginsWindowTab(cb) {
37412 const store2 = getStore();
37413 if (!store2) {
37414 return () => {
37415 };
37416 }
37417 return store2.subscribe((state2) => cb({ ...state2 }));
37418 }
37419 const tabTarget = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
37420 __proto__: null,
37421 consumePluginsWindowTab,
37422 setPluginsWindowTab,
37423 subscribePluginsWindowTab
37424 }, Symbol.toStringTag, { value: "Module" }));
37425 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}}`;
37426 const _WpdProgressBar = class _WpdProgressBar extends Component {
37427 constructor() {
37428 super(...arguments);
37429 this._ownedAriaLabel = null;
37430 }
37431 render() {
37432 return html`<div class="root" part="root">
37433 <div class="header" part="header" hidden>
37434 <span class="label" part="label"></span>
37435 <span class="percent" part="percent"></span>
37436 </div>
37437 <div class="track" part="track">
37438 <div class="fill" part="fill"></div>
37439 </div>
37440 </div>`;
37441 }
37442 requestUpdate() {
37443 super.requestUpdate();
37444 queueMicrotask(() => this._paint());
37445 }
37446 connectedCallback() {
37447 super.connectedCallback();
37448 queueMicrotask(() => this._paint());
37449 }
37450 _paint() {
37451 const root = this.shadowRoot;
37452 if (!root) {
37453 return;
37454 }
37455 const max = this._readMax();
37456 const indeterminate = this.hasAttribute("indeterminate") || max <= 0;
37457 const value = indeterminate ? 0 : this._readValue(max);
37458 const ratio = indeterminate ? 0 : value / max;
37459 const percent = Math.round(ratio * 100);
37460 const label = this.getAttribute("label") ?? "";
37461 const showPercent = this.hasAttribute("show-percent");
37462 const fill = root.querySelector(".fill");
37463 if (fill && !indeterminate) {
37464 fill.style.width = `${(ratio * 100).toFixed(2)}%`;
37465 } else if (fill && indeterminate) {
37466 fill.style.removeProperty("width");
37467 }
37468 const header = root.querySelector(".header");
37469 const labelEl = root.querySelector(".label");
37470 const percentEl = root.querySelector(".percent");
37471 if (header && labelEl && percentEl) {
37472 const visible = label || showPercent && !indeterminate;
37473 header.hidden = !visible;
37474 labelEl.textContent = label;
37475 percentEl.hidden = !(showPercent && !indeterminate);
37476 percentEl.textContent = `${percent}%`;
37477 }
37478 this._syncAria(max, value, indeterminate, label);
37479 const track = root.querySelector(".track");
37480 if (track) {
37481 track.setAttribute("role", "progressbar");
37482 track.setAttribute("aria-valuemin", "0");
37483 if (indeterminate) {
37484 track.removeAttribute("aria-valuenow");
37485 track.removeAttribute("aria-valuemax");
37486 } else {
37487 track.setAttribute("aria-valuemax", String(max));
37488 track.setAttribute("aria-valuenow", String(value));
37489 }
37490 if (label) {
37491 track.setAttribute("aria-label", label);
37492 } else {
37493 track.removeAttribute("aria-label");
37494 }
37495 }
37496 }
37497 _syncAria(max, value, indeterminate, label) {
37498 this.setAttribute("role", "progressbar");
37499 this.setAttribute("aria-valuemin", "0");
37500 if (indeterminate) {
37501 this.removeAttribute("aria-valuenow");
37502 this.removeAttribute("aria-valuemax");
37503 } else {
37504 this.setAttribute("aria-valuemax", String(max));
37505 this.setAttribute("aria-valuenow", String(value));
37506 }
37507 const existing = this.getAttribute("aria-label");
37508 if (label) {
37509 if (existing === null || existing === this._ownedAriaLabel) {
37510 this.setAttribute("aria-label", label);
37511 this._ownedAriaLabel = label;
37512 }
37513 } else if (existing !== null && existing === this._ownedAriaLabel) {
37514 this.removeAttribute("aria-label");
37515 this._ownedAriaLabel = null;
37516 }
37517 }
37518 _readMax() {
37519 const attr = this.getAttribute("max");
37520 if (attr === null) {
37521 return 100;
37522 }
37523 const raw = parseFloat(attr);
37524 return Number.isFinite(raw) ? raw : 100;
37525 }
37526 _readValue(max) {
37527 const raw = parseFloat(this.getAttribute("value") ?? "0");
37528 if (!Number.isFinite(raw)) {
37529 return 0;
37530 }
37531 if (raw < 0) {
37532 return 0;
37533 }
37534 if (raw > max) {
37535 return max;
37536 }
37537 return raw;
37538 }
37539 };
37540 _WpdProgressBar.props = [
37541 "value",
37542 "max",
37543 "indeterminate",
37544 "tone",
37545 "label",
37546 "showPercent"
37547 ];
37548 _WpdProgressBar.styles = [styles];
37549 _WpdProgressBar.help = {
37550 title: "Progress bar",
37551 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.",
37552 status: "experimental",
37553 since: "0.31.0",
37554 props: [
37555 {
37556 name: "value",
37557 type: "number",
37558 default: "0",
37559 description: "Current progress. Clamped to `[0, max]`."
37560 },
37561 {
37562 name: "max",
37563 type: "number",
37564 default: "100",
37565 description: "Maximum value. Setting `max <= 0` forces indeterminate."
37566 },
37567 {
37568 name: "indeterminate",
37569 type: "boolean",
37570 description: "Show the sweeping indeterminate animation instead of a value-driven fill. The `value` attribute is ignored while this is set."
37571 },
37572 {
37573 name: "tone",
37574 type: '"default" | "success" | "warning" | "danger"',
37575 default: "default",
37576 description: "Tints the fill from the shared status palette."
37577 },
37578 {
37579 name: "label",
37580 type: "string",
37581 description: "Optional inline label rendered above the track. Also wired into `aria-label` when set."
37582 },
37583 {
37584 name: "show-percent",
37585 type: "boolean",
37586 description: "Render a right-aligned percent readout next to the label. Only meaningful in determinate mode."
37587 }
37588 ],
37589 cssProps: [
37590 {
37591 name: "--wpd-progress-track-bg",
37592 default: "var(--desktop-mode-control-bg, rgba(0,0,0,0.08))"
37593 },
37594 {
37595 name: "--wpd-progress-fill",
37596 default: "var(--wp-admin-theme-color, #2271b1)"
37597 },
37598 { name: "--wpd-progress-height", default: "6px" },
37599 { name: "--wpd-progress-radius", default: "999px" },
37600 { name: "--wpd-progress-label-color", default: "inherit" },
37601 { name: "--wpd-progress-label-size", default: "12px" },
37602 { name: "--wpd-progress-label-gap", default: "4px" }
37603 ],
37604 example: html`<wpd-progress-bar
37605 value="42"
37606 label="Uploading hero.jpg"
37607 show-percent
37608 ></wpd-progress-bar>`
37609 };
37610 let WpdProgressBar = _WpdProgressBar;
37611 defineComponent("wpd-progress-bar", WpdProgressBar);
37612 const ROWS = /* @__PURE__ */ new Map();
37613 let panel = null;
37614 function mountUploadProgressHud() {
37615 if (document.body.hasAttribute("data-desktop-mode-suppress-upload-hud")) {
37616 return;
37617 }
37618 if (window.__wpdUploadHud) {
37619 return;
37620 }
37621 window.__wpdUploadHud = true;
37622 const ns = "desktop-mode/os-file-drop-hud";
37623 addAction(
37624 FILE_DROP_HOOKS.UPLOAD_STARTED,
37625 ns,
37626 (payload) => onStarted(payload.file, payload.fields, payload.abort)
37627 );
37628 addAction(
37629 FILE_DROP_HOOKS.UPLOAD_PROGRESS,
37630 ns,
37631 (payload) => onProgress(
37632 payload.file,
37633 payload.loaded,
37634 payload.total,
37635 payload.indeterminate
37636 )
37637 );
37638 addAction(
37639 FILE_DROP_HOOKS.AFTER_UPLOAD,
37640 ns,
37641 (payload) => onComplete(payload.file, payload.fields, payload.result)
37642 );
37643 addAction(
37644 FILE_DROP_HOOKS.UPLOAD_FAILED,
37645 ns,
37646 (payload) => onFailed(payload.file, payload.error)
37647 );
37648 }
37649 function onStarted(file, fields, abort) {
37650 const p = ensurePanel();
37651 const row = document.createElement("div");
37652 row.className = "desktop-mode-upload-hud__row";
37653 const meta = document.createElement("div");
37654 meta.className = "desktop-mode-upload-hud__meta";
37655 const name = document.createElement("div");
37656 name.className = "desktop-mode-upload-hud__name";
37657 name.textContent = fields.filename || file.name;
37658 name.title = fields.filename || file.name;
37659 const statusEl = document.createElement("div");
37660 statusEl.className = "desktop-mode-upload-hud__status";
37661 statusEl.textContent = "Uploading…";
37662 meta.append(name, statusEl);
37663 const bar = document.createElement("wpd-progress-bar");
37664 bar.setAttribute("indeterminate", "");
37665 bar.setAttribute("show-percent", "");
37666 const actions = document.createElement("div");
37667 actions.className = "desktop-mode-upload-hud__actions";
37668 const cancelBtn = document.createElement("wpd-button");
37669 cancelBtn.setAttribute("variant", "tertiary");
37670 cancelBtn.setAttribute("size", "small");
37671 cancelBtn.textContent = "Cancel";
37672 cancelBtn.addEventListener("click", () => {
37673 const r = ROWS.get(file);
37674 if (!r) {
37675 return;
37676 }
37677 if (r.state === "running") {
37678 r.statusEl.textContent = "Cancelling…";
37679 r.cancelBtn.disabled = true;
37680 r.abort();
37681 } else {
37682 dismissRow(r);
37683 }
37684 });
37685 actions.appendChild(cancelBtn);
37686 row.append(meta, bar, actions);
37687 p.querySelector(".desktop-mode-upload-hud__list").appendChild(row);
37688 ROWS.set(file, {
37689 file,
37690 abort,
37691 root: row,
37692 bar,
37693 statusEl,
37694 cancelBtn,
37695 state: "running",
37696 lingerTimer: null
37697 });
37698 updateHeader();
37699 }
37700 function onProgress(file, loaded, total, indeterminate) {
37701 const r = ROWS.get(file);
37702 if (!r || r.state !== "running") {
37703 return;
37704 }
37705 if (indeterminate || total <= 0) {
37706 r.bar.setAttribute("indeterminate", "");
37707 r.statusEl.textContent = `${formatBytes$1(loaded)} sent`;
37708 } else {
37709 r.bar.removeAttribute("indeterminate");
37710 r.bar.setAttribute("max", String(total));
37711 r.bar.setAttribute("value", String(loaded));
37712 r.statusEl.textContent = `${formatBytes$1(loaded)} / ${formatBytes$1(total)}`;
37713 }
37714 }
37715 function onComplete(file, fields, result) {
37716 const r = ROWS.get(file);
37717 if (!r) {
37718 return;
37719 }
37720 r.state = "success";
37721 r.bar.removeAttribute("indeterminate");
37722 r.bar.setAttribute("value", "100");
37723 r.bar.setAttribute("max", "100");
37724 r.bar.setAttribute("tone", "success");
37725 r.statusEl.textContent = "Uploaded";
37726 r.cancelBtn.textContent = "Dismiss";
37727 r.lingerTimer = setTimeout(() => dismissRow(r), 2500);
37728 updateHeader();
37729 activity.publish("desktop-mode/upload-hud-complete", {
37730 filename: fields.filename || result.filename,
37731 attachmentId: result.id
37732 });
37733 }
37734 function onFailed(file, error) {
37735 const r = ROWS.get(file);
37736 if (!r) {
37737 return;
37738 }
37739 r.bar.removeAttribute("indeterminate");
37740 r.bar.setAttribute("tone", "danger");
37741 r.cancelBtn.textContent = "Dismiss";
37742 r.cancelBtn.disabled = false;
37743 if (error.name === "UploadAbortedError") {
37744 r.state = "aborted";
37745 r.statusEl.textContent = "Cancelled";
37746 } else {
37747 r.state = "failed";
37748 r.statusEl.textContent = error.message || "Upload failed";
37749 }
37750 updateHeader();
37751 }
37752 function dismissRow(r) {
37753 if (r.lingerTimer) {
37754 clearTimeout(r.lingerTimer);
37755 }
37756 ROWS.delete(r.file);
37757 r.root.remove();
37758 updateHeader();
37759 if (ROWS.size === 0 && panel) {
37760 panel.hidden = true;
37761 }
37762 }
37763 function ensurePanel() {
37764 if (panel && panel.isConnected) {
37765 panel.hidden = false;
37766 return panel;
37767 }
37768 const p = document.createElement("div");
37769 p.className = "desktop-mode-upload-hud";
37770 p.setAttribute("role", "region");
37771 p.setAttribute("aria-label", "Uploads");
37772 const header = document.createElement("div");
37773 header.className = "desktop-mode-upload-hud__header";
37774 const title = document.createElement("div");
37775 title.className = "desktop-mode-upload-hud__title";
37776 title.textContent = "Uploads";
37777 const closeBtn = document.createElement("button");
37778 closeBtn.type = "button";
37779 closeBtn.className = "desktop-mode-upload-hud__close";
37780 closeBtn.setAttribute("aria-label", "Hide upload panel");
37781 closeBtn.textContent = "×";
37782 closeBtn.addEventListener("click", () => {
37783 for (const r of [...ROWS.values()]) {
37784 if (r.state !== "running") {
37785 dismissRow(r);
37786 }
37787 }
37788 if (ROWS.size === 0) {
37789 p.hidden = true;
37790 }
37791 });
37792 header.append(title, closeBtn);
37793 const list2 = document.createElement("div");
37794 list2.className = "desktop-mode-upload-hud__list";
37795 p.append(header, list2);
37796 document.body.appendChild(p);
37797 panel = p;
37798 return p;
37799 }
37800 function updateHeader() {
37801 if (!panel) {
37802 return;
37803 }
37804 const title = panel.querySelector(
37805 ".desktop-mode-upload-hud__title"
37806 );
37807 if (!title) {
37808 return;
37809 }
37810 const total = ROWS.size;
37811 const running = [...ROWS.values()].filter((r) => r.state === "running").length;
37812 if (running > 0) {
37813 title.textContent = running === total ? `Uploading ${running} file${running === 1 ? "" : "s"}…` : `${running} of ${total} uploading…`;
37814 } else if (total > 0) {
37815 title.textContent = `Uploads (${total})`;
37816 } else {
37817 title.textContent = "Uploads";
37818 }
37819 }
37820 function mountMediaLibraryRefresher() {
37821 if (document.body.hasAttribute(
37822 "data-desktop-mode-suppress-media-library-refresh"
37823 )) {
37824 return;
37825 }
37826 const sentinel = window;
37827 if (sentinel.__wpdMediaLibraryRefresher) {
37828 return;
37829 }
37830 sentinel.__wpdMediaLibraryRefresher = true;
37831 addAction(
37832 FILE_DROP_HOOKS.AFTER_UPLOAD,
37833 "desktop-mode/os-file-drop-library-refresh",
37834 () => refreshOpenLibraries()
37835 );
37836 }
37837 function refreshOpenLibraries() {
37838 const iframes = document.querySelectorAll("iframe");
37839 for (const frame of Array.from(iframes)) {
37840 if (!isMediaLibraryUrl(resolveIframeUrl(frame))) {
37841 continue;
37842 }
37843 try {
37844 frame.contentWindow?.location.reload();
37845 } catch {
37846 const reloadHref = resolveIframeUrl(frame);
37847 if (reloadHref) {
37848 frame.setAttribute("src", reloadHref);
37849 }
37850 }
37851 }
37852 }
37853 function resolveIframeUrl(frame) {
37854 try {
37855 return frame.contentWindow?.location.href ?? frame.src ?? "";
37856 } catch {
37857 return frame.src ?? "";
37858 }
37859 }
37860 function isMediaLibraryUrl(url) {
37861 if (!url) {
37862 return false;
37863 }
37864 return /\/wp-admin\/upload\.php(?:[?#]|$)/.test(url);
37865 }
37866 function bootOsFileDrop(args) {
37867 const config = args.config || {
37868 enabled: false,
37869 allowedMimes: [],
37870 maxSize: 0
37871 };
37872 mountUploadProgressHud();
37873 mountMediaLibraryRefresher();
37874 mountOsFileDropManager({
37875 config,
37876 mediaUrl: args.mediaUrl,
37877 restNonce: args.restNonce,
37878 filesUrl: args.filesUrl,
37879 storage: args.storage,
37880 openDialog: async (entries, ctx, extra) => {
37881 const { openUploadDialog: openUploadDialog2 } = await Promise.resolve().then(() => dialog);
37882 await openUploadDialog2({
37883 entries,
37884 context: ctx,
37885 mediaUrl: args.mediaUrl,
37886 restNonce: args.restNonce,
37887 filesUrl: args.filesUrl,
37888 storage: args.storage,
37889 forceDesktop: extra?.forceDesktop,
37890 emptyDirs: extra?.emptyDirs,
37891 mediaMaxBytes: config.maxSize
37892 });
37893 }
37894 });
37895 }
37896 const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
37897 __proto__: null,
37898 FILE_DROP_HOOKS,
37899 bootOsFileDrop
37900 }, Symbol.toStringTag, { value: "Module" }));
37901 exports.clampGeometryToViewport = clampGeometryToViewport;
37902 Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
37903 return exports;
37904 }({});
37905