PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.8
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 / includes / admin-bar.php

admin-bar.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.8, at includes/admin-bar.php

780 lines 28.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode admin-bar toggle.
4 *
5 * Adds the "Switch to Desktop Mode" button to the admin bar's top-right
6 * area and wires its click handler to the save-desktop-mode AJAX endpoint.
7 *
8 * @package WPDesktopMode
9 */
10
11 defined( 'ABSPATH' ) || exit;
12
13 /**
14 * Adds the desktop mode toggle to the admin bar.
15 *
16 * Allows users to switch between classic admin and desktop mode,
17 * which renders admin screens in draggable, resizable windows.
18 *
19 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
20 */
21 function desktop_mode_admin_bar_toggle( $wp_admin_bar ) {
22 if ( ! is_admin() || ! is_user_logged_in() ) {
23 return;
24 }
25
26 // "Active" means "the user is *currently viewing* desktop mode",
27 // not "the preference is enabled in user meta." These diverge on
28 // requests carrying the per-request classic override
29 // (`?desktop_mode_classic=1`) — the user's meta may be '1' but the
30 // page they're looking at right now is classic admin. If we went
31 // by meta alone, the toggle here would read "Switch to Classic
32 // Admin" on a page that's already classic, and the user's first
33 // click would disable desktop mode entirely (redirecting to
34 // classic admin) instead of taking them back into the shell.
35 // The second click would then re-enable it — a two-click trap.
36 $is_active = desktop_mode_is_enabled() && ! desktop_mode_is_classic_request();
37 $label = $is_active
38 ? __( 'Switch to Classic Admin', 'desktop-mode' )
39 : __( 'Switch to Desktop Mode', 'desktop-mode' );
40
41 $wp_admin_bar->add_node(
42 array(
43 'parent' => 'top-secondary',
44 'id' => 'desktop-mode-toggle',
45 'title' => '<span class="ab-icon dashicons dashicons-desktop" aria-hidden="true"></span>'
46 . '<span class="ab-label">' . $label . '</span>',
47 'href' => '#',
48 'meta' => array(
49 'class' => $is_active ? 'desktop-mode-active' : '',
50 'tabindex' => 0,
51 'title' => $label,
52 ),
53 )
54 );
55
56 // Fullscreen toggle — sits next to "Switch to Classic Admin" so the
57 // shell can occupy the whole screen without browser chrome. Only
58 // makes sense in desktop mode; kept out of classic where it would
59 // just be a redundant browser-fullscreen shortcut.
60 if ( $is_active ) {
61 $wp_admin_bar->add_node(
62 array(
63 'parent' => 'top-secondary',
64 'id' => 'desktop-fullscreen',
65 'title' => '<span class="ab-icon dashicons dashicons-fullscreen-alt" aria-hidden="true"></span>',
66 'href' => '#',
67 'meta' => array(
68 'class' => 'desktop-fullscreen-btn',
69 'title' => __( 'Enter fullscreen', 'desktop-mode' ),
70 'tabindex' => 0,
71 ),
72 )
73 );
74 }
75
76 // Layout menu — only surfaced when the user is actually viewing
77 // the desktop shell (the actions don't make sense in classic
78 // admin, which has no windows to arrange). Parent renders as a
79 // dashicon with a hover-opened submenu; each child is routed to
80 // `wp.desktop.windowManager.*` by the inline JS.
81 //
82 // Sits next to Fullscreen so the two shell-state actions group
83 // visually. Ask AI, then the help/meta cluster (Keyboard shortcuts
84 // + Report a bug) follow, with the help/meta pair anchored to the
85 // far right of the secondary bar next to the user identity menu.
86 if ( $is_active ) {
87 $wp_admin_bar->add_node(
88 array(
89 'parent' => 'top-secondary',
90 'id' => 'desktop-layout-menu',
91 'title' => '<span class="ab-icon dashicons dashicons-grid-view" aria-hidden="true"></span>',
92 'href' => '#',
93 'meta' => array(
94 'title' => __( 'Arrange windows', 'desktop-mode' ),
95 'tabindex' => 0,
96 ),
97 )
98 );
99 $wp_admin_bar->add_node(
100 array(
101 'parent' => 'desktop-layout-menu',
102 'id' => 'desktop-layout-cascade',
103 'title' => esc_html__( 'Cascade', 'desktop-mode' ),
104 'href' => '#',
105 'meta' => array(
106 'class' => 'desktop-mode-layout-action',
107 'title' => __( 'Lay all windows out from top-left, offset so every title bar stays visible.', 'desktop-mode' ),
108 ),
109 )
110 );
111 $wp_admin_bar->add_node(
112 array(
113 'parent' => 'desktop-layout-menu',
114 'id' => 'desktop-layout-overview',
115 'title' => esc_html__( 'Overview', 'desktop-mode' ),
116 'href' => '#',
117 'meta' => array(
118 'class' => 'desktop-mode-layout-action',
119 'title' => __( 'Zoom out to see every window at once. Click one to focus it.', 'desktop-mode' ),
120 ),
121 )
122 );
123 // Snap-to-grid toggle. Renders as a checkbox-style entry that
124 // must NOT dismiss the parent menu on click — see the inline
125 // JS below for the stop-propagation handling. Initial check
126 // state is painted from the persisted preference once the
127 // shell has booted.
128 $wp_admin_bar->add_node(
129 array(
130 'parent' => 'desktop-layout-menu',
131 'id' => 'desktop-layout-snap',
132 'title' => '<span class="desktop-mode-layout-checkbox" aria-hidden="true">☐</span> '
133 . esc_html__( 'Snap to grid', 'desktop-mode' ),
134 'href' => '#',
135 'meta' => array(
136 'class' => 'desktop-mode-layout-snap',
137 'title' => __( 'Snap windows to a grid while dragging or resizing.', 'desktop-mode' ),
138 ),
139 )
140 );
141 $wp_admin_bar->add_node(
142 array(
143 'parent' => 'desktop-layout-menu',
144 'id' => 'desktop-layout-tile',
145 'title' => esc_html__( 'Tile all windows', 'desktop-mode' ),
146 'href' => '#',
147 'meta' => array(
148 'class' => 'desktop-mode-layout-action',
149 'title' => __( 'Pack every window into an evenly tiled grid that fills the desktop.', 'desktop-mode' ),
150 ),
151 )
152 );
153
154 /**
155 * Filter the list of custom arrange-menu items contributed by
156 * plugins. Each entry becomes an additional node under the
157 * "Arrange" submenu, rendered with the same styling as the
158 * built-in Cascade / Overview / Tile items. Clicking a custom
159 * item dispatches the JS action `desktop-mode.arrange.custom-action`
160 * with payload `{ id }` — plugins subscribe via
161 * `wp.hooks.addAction()` and run their own arrangement logic.
162 *
163 * Each item is an associative array:
164 *
165 * 'id' string Unique slug (letters, digits, dashes).
166 * 'title' string Menu label (already translated).
167 * 'description' string Optional tooltip / aria description.
168 * 'position' int Optional sort key; lower sorts earlier.
169 * Built-ins are effectively at 0-3; use
170 * 10+ to append.
171 *
172 * Entries with missing/invalid `id` or `title` are dropped.
173 *
174 * @param array $items Existing custom items (default empty).
175 */
176 $custom = apply_filters( 'desktop_mode_arrange_menu_items', array() );
177 if ( is_array( $custom ) ) {
178 // Stable sort by `position` (default 10 — after built-ins),
179 // preserving registration order within a tie.
180 $sortable = array();
181 foreach ( $custom as $index => $item ) {
182 if ( ! is_array( $item ) ) {
183 continue;
184 }
185 $id = isset( $item['id'] ) ? sanitize_key( (string) $item['id'] ) : '';
186 $title = isset( $item['title'] ) ? (string) $item['title'] : '';
187 if ( '' === $id || '' === $title ) {
188 continue;
189 }
190 $sortable[] = array(
191 'id' => $id,
192 'title' => $title,
193 'description' => isset( $item['description'] ) ? (string) $item['description'] : '',
194 'position' => isset( $item['position'] ) && is_numeric( $item['position'] )
195 ? (int) $item['position']
196 : 10,
197 'index' => $index,
198 );
199 }
200 usort(
201 $sortable,
202 static function ( $a, $b ) {
203 if ( $a['position'] === $b['position'] ) {
204 return $a['index'] - $b['index'];
205 }
206 return $a['position'] - $b['position'];
207 }
208 );
209 foreach ( $sortable as $item ) {
210 // The custom id is round-tripped through the DOM id
211 // (stripping the `desktop-layout-custom-` prefix in the
212 // click handler). Keeps us inside the documented
213 // WP_Admin_Bar::add_node meta surface — no non-standard
214 // attributes, no custom render callbacks.
215 $wp_admin_bar->add_node(
216 array(
217 'parent' => 'desktop-layout-menu',
218 'id' => 'desktop-layout-custom-' . $item['id'],
219 'title' => esc_html( $item['title'] ),
220 'href' => '#',
221 'meta' => array(
222 'class' => 'desktop-mode-layout-action desktop-mode-layout-custom',
223 'title' => $item['description'],
224 ),
225 )
226 );
227 }
228 }
229 }
230
231
232 // "Keyboard shortcuts" trigger — shown only when desktop mode is
233 // active. Clicking toggles the keyboard-shortcuts popover wired by
234 // assets/js/admin-bar.js (wireShortcutsPopover); the popover content
235 // is translated server-side and shipped via the `shortcuts` key of
236 // the desktopModeAdminBar config blob below.
237 if ( $is_active ) {
238 $wp_admin_bar->add_node(
239 array(
240 'parent' => 'top-secondary',
241 'id' => 'desktop-help',
242 'title' => '<span class="ab-icon desktop-mode-keyboard-icon" aria-hidden="true"></span>',
243 'href' => '#',
244 'meta' => array(
245 'class' => 'desktop-help-btn',
246 'title' => __( 'Keyboard shortcuts', 'desktop-mode' ),
247 'tabindex' => 0,
248 ),
249 )
250 );
251 }
252
253 // "Report a bug" trigger — shown only when desktop mode is active.
254 // Clicking dispatches a `desktop-mode-open-bug-report` document
255 // CustomEvent that the shell listens for and answers by opening the
256 // Bug Report native window. PHP doesn't know the JS side exists; the
257 // event lets us add the button without coupling to any specific
258 // shell module. Anchored to the far right of the secondary bar so
259 // the meta/help cluster (Keyboard shortcuts + Report a bug) sits
260 // next to the user identity menu — matches the convention used by
261 // most SaaS products (Help / "?" at the user-menu corner).
262 if ( $is_active ) {
263 $wp_admin_bar->add_node(
264 array(
265 'parent' => 'top-secondary',
266 'id' => 'desktop-bug-report',
267 'title' => '<span class="ab-icon dashicons dashicons-buddicons-replies" aria-hidden="true"></span>',
268 'href' => '#',
269 'meta' => array(
270 'class' => 'desktop-bug-report-btn',
271 'title' => __( 'Open the Bug Report window', 'desktop-mode' ),
272 'tabindex' => 0,
273 ),
274 )
275 );
276 }
277 }
278 add_action( 'admin_bar_menu', 'desktop_mode_admin_bar_toggle', 190 );
279
280 /**
281 * Enqueues the CSS and JS for the desktop mode toggle.
282 *
283 * The CSS is inline, attached to the `admin-bar` style handle so it always
284 * ships with the admin bar itself — no matter which admin screen is showing.
285 * The JS is the external assets/js/admin-bar.js bundle, registered as
286 * `desktop-mode-admin-bar` with `admin-bar` as a dependency; its config is
287 * emitted as an inline JSON literal `before` the script.
288 */
289 function desktop_mode_enqueue_toggle_assets() {
290 if ( ! is_admin() || ! is_user_logged_in() ) {
291 return;
292 }
293
294 $css = '
295 #wpadminbar #wp-admin-bar-desktop-mode-toggle > .ab-item,
296 #wpadminbar #wp-admin-bar-desktop-layout-menu > .ab-item,
297 #wpadminbar #wp-admin-bar-desktop-fullscreen > .ab-item,
298 #wpadminbar #wp-admin-bar-desktop-bug-report > .ab-item,
299 #wpadminbar #wp-admin-bar-desktop-help > .ab-item {
300 display: inline-flex;
301 align-items: center;
302 gap: 6px;
303 }
304 #wpadminbar #wp-admin-bar-desktop-mode-toggle .ab-icon,
305 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-icon,
306 #wpadminbar #wp-admin-bar-desktop-fullscreen .ab-icon,
307 #wpadminbar #wp-admin-bar-desktop-bug-report .ab-icon,
308 #wpadminbar #wp-admin-bar-desktop-help .ab-icon {
309 float: none;
310 margin: 0;
311 padding: 0;
312 display: inline-flex;
313 align-items: center;
314 justify-content: center;
315 width: 20px;
316 height: 20px;
317 }
318
319 #wp-admin-bar-desktop-mode-toggle .ab-icon.dashicons,
320 #wp-admin-bar-desktop-layout-menu .ab-icon.dashicons {
321 font: normal 20px/1 dashicons;
322 -webkit-font-smoothing: antialiased;
323 -moz-osx-font-smoothing: grayscale;
324 }
325 #wp-admin-bar-desktop-mode-toggle .ab-icon.dashicons::before {
326 content: "\f472";
327 top: 0;
328 position: static;
329 }
330 #wp-admin-bar-desktop-layout-menu .ab-icon.dashicons::before {
331 /* dashicons-grid-view */
332 content: "\f509";
333 top: 0;
334 position: static;
335 }
336 #wp-admin-bar-desktop-mode-toggle.desktop-mode-active .ab-icon.dashicons::before {
337 /* Inherit the admin-bar text color so the active state
338 matches the +New, Home, Comments dashicons. Previously
339 hardcoded #72aee6, which forced blue regardless of the
340 profile color scheme. */
341 color: inherit;
342 }
343 @media screen and (max-width: 782px) {
344 #wp-admin-bar-desktop-mode-toggle .ab-label {
345 display: none;
346 }
347 }
348
349 /* Fullscreen admin-bar button — same dashicons rendering pattern
350 as the toggle. Icon swaps between fullscreen-alt (enter) and
351 fullscreen-exit-alt (exit) via the .is-fullscreen class set by
352 admin-bar.js on `fullscreenchange`. */
353 #wp-admin-bar-desktop-fullscreen .ab-icon.dashicons {
354 font: normal 20px/1 dashicons;
355 -webkit-font-smoothing: antialiased;
356 -moz-osx-font-smoothing: grayscale;
357 }
358 #wp-admin-bar-desktop-fullscreen .ab-icon.dashicons::before {
359 /* dashicons-fullscreen-alt */
360 content: "\f211";
361 top: 0;
362 position: static;
363 }
364 #wp-admin-bar-desktop-fullscreen.is-fullscreen .ab-icon.dashicons::before {
365 /* dashicons-fullscreen-exit-alt */
366 content: "\f212";
367 }
368 /* Defuse the implicit-drag-on-anchor click eater. WP renders
369 admin-bar items as <a href="#">, which is implicitly
370 draggable; tiny pointer jitter on click commits to a native
371 link-drag (the floating "Exit fullscreen / http://…#" ghost
372 the user sees) and the click handler never runs. admin-bar.js
373 also sets draggable="false" on the <a>; user-drag: none here
374 covers the WebKit path where the attribute alone is not
375 honoured reliably. */
376 #wp-admin-bar-desktop-fullscreen .ab-item,
377 #wp-admin-bar-desktop-fullscreen .ab-item * {
378 -webkit-user-drag: none;
379 user-drag: none;
380 -webkit-user-select: none;
381 user-select: none;
382 }
383 /* Hide admin-bar items that sit closest to the right corner
384 while in browser fullscreen. The corner is a reveal hot zone
385 the OS / browser reserve for menu-bar / window-controls,
386 where pointer events get intercepted before reaching the
387 page. Hiding these two shifts the Fullscreen toggle further
388 from the corner so its own click lands. Toggled by a body
389 class admin-bar.js sets on `fullscreenchange`. */
390 body.desktop-mode-browser-fs #wp-admin-bar-desktop-mode-toggle,
391 body.desktop-mode-browser-fs #wp-admin-bar-desktop-bug-report {
392 display: none;
393 }
394
395 /* Bug Report admin-bar button — same dashicons rendering pattern
396 as the toggle. dashicons-buddicons-replies is a speech bubble
397 with stylized lines that reads as a comment / report glyph at
398 admin-bar size, while dashicons-bug is too literal and small. */
399 #wp-admin-bar-desktop-bug-report .ab-icon.dashicons {
400 font: normal 20px/1 dashicons;
401 -webkit-font-smoothing: antialiased;
402 -moz-osx-font-smoothing: grayscale;
403 }
404 #wp-admin-bar-desktop-bug-report .ab-icon.dashicons::before {
405 /* dashicons-buddicons-replies — same glyph as the dock tile so
406 the two surfaces (admin-bar + dock) match. */
407 content: "\f451";
408 top: 0;
409 position: static;
410 }
411
412 /* Keyboard shortcuts admin-bar button. Dashicons has no keyboard
413 glyph, so we paint a small inline SVG via CSS `mask` with
414 `background-color: currentColor` — that way the icon inherits
415 the admin-bar text color across every WP profile scheme
416 without us hardcoding a fill. */
417 #wp-admin-bar-desktop-help .ab-icon.desktop-mode-keyboard-icon {
418 background-color: currentColor;
419 -webkit-mask: url( "data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'black\' stroke-width=\'1.6\' stroke-linecap=\'round\' stroke-linejoin=\'round\'><rect x=\'2.5\' y=\'6\' width=\'19\' height=\'12\' rx=\'2.2\'/><path d=\'M6 10h0M10 10h0M14 10h0M18 10h0M6 14h0M10 14h4M18 14h0\'/></svg>" ) no-repeat center / 20px 20px;
420 mask: url( "data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'black\' stroke-width=\'1.6\' stroke-linecap=\'round\' stroke-linejoin=\'round\'><rect x=\'2.5\' y=\'6\' width=\'19\' height=\'12\' rx=\'2.2\'/><path d=\'M6 10h0M10 10h0M14 10h0M18 10h0M6 14h0M10 14h4M18 14h0\'/></svg>" ) no-repeat center / 20px 20px;
421 }
422
423 /* Keyboard-shortcuts floating popover — anchored under the
424 keyboard button. Styled to match the Arrange submenu (light
425 bg on the otherwise-dark admin bar, soft shadow, rounded
426 corners). Built dynamically in admin-bar.js from translated
427 content shipped via `desktopModeAdminBar.shortcuts`. */
428 #wpadminbar #wp-admin-bar-desktop-help {
429 position: relative;
430 }
431 #wpadminbar .desktop-mode-shortcuts-popover {
432 display: none;
433 position: absolute;
434 top: 100%;
435 right: 0;
436 margin-top: 0;
437 padding: 12px 14px;
438 min-width: 460px;
439 max-width: min( 90vw, 720px );
440 background: var( --wpd-surface, var( --desktop-mode-window-bg, #fff ) );
441 color: var( --wpd-fg, #1d2327 );
442 border: 1px solid var( --wpd-border, var( --desktop-mode-window-border, #c3c4c7 ) );
443 border-radius: 8px;
444 box-shadow: 0 8px 24px rgba( 0, 0, 0, 0.18 ),
445 0 2px 6px rgba( 0, 0, 0, 0.08 );
446 z-index: 99999;
447 font-size: 12px;
448 line-height: 1.4;
449 text-align: left;
450 }
451 #wpadminbar .desktop-mode-shortcuts-popover.is-open {
452 display: block;
453 }
454 #wpadminbar .desktop-mode-shortcuts-popover__section + .desktop-mode-shortcuts-popover__section {
455 margin-top: 12px;
456 }
457 #wpadminbar .desktop-mode-shortcuts-popover__heading {
458 margin: 0 0 6px;
459 padding: 0;
460 font-size: 11px;
461 font-weight: 600;
462 letter-spacing: 0.04em;
463 text-transform: uppercase;
464 color: var( --wpd-fg-muted, #50575e );
465 line-height: 1.2;
466 }
467 #wpadminbar .desktop-mode-shortcuts-popover__table {
468 width: 100%;
469 border-collapse: collapse;
470 color: inherit;
471 }
472 #wpadminbar .desktop-mode-shortcuts-popover__table th,
473 #wpadminbar .desktop-mode-shortcuts-popover__table td {
474 padding: 6px 8px;
475 vertical-align: middle;
476 text-align: left;
477 border-bottom: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.06 ) );
478 color: inherit;
479 font-weight: 400;
480 font-size: 12px;
481 line-height: 1.4;
482 }
483 #wpadminbar .desktop-mode-shortcuts-popover__table th {
484 font-weight: 600;
485 font-size: 11px;
486 color: var( --wpd-fg-muted, #50575e );
487 letter-spacing: 0.02em;
488 }
489 #wpadminbar .desktop-mode-shortcuts-popover__table tbody tr:last-child td {
490 border-bottom: none;
491 }
492 #wpadminbar .desktop-mode-shortcuts-popover__key-cell {
493 white-space: nowrap;
494 }
495 #wpadminbar .desktop-mode-shortcuts-popover__note {
496 color: var( --wpd-fg-muted, #50575e );
497 font-size: 11px;
498 }
499 #wpadminbar .desktop-mode-shortcuts-popover__list {
500 margin: 0;
501 padding: 0;
502 list-style: none;
503 display: flex;
504 flex-direction: column;
505 gap: 4px;
506 }
507 #wpadminbar .desktop-mode-shortcuts-popover__item {
508 display: grid;
509 grid-template-columns: 120px 1fr;
510 align-items: center;
511 gap: 12px;
512 padding: 4px 8px;
513 border-radius: 4px;
514 background: var( --wpd-surface-sunken, rgba( 0, 0, 0, 0.03 ) );
515 }
516 #wpadminbar .desktop-mode-shortcuts-popover__keys {
517 display: inline-flex;
518 align-items: center;
519 gap: 4px;
520 flex-wrap: wrap;
521 }
522 #wpadminbar .desktop-mode-shortcuts-popover__kbd {
523 display: inline-flex;
524 align-items: center;
525 justify-content: center;
526 min-width: 22px;
527 height: 20px;
528 padding: 0 5px;
529 border: 1px solid var( --wpd-border-strong, rgba( 0, 0, 0, 0.18 ) );
530 border-bottom-width: 2px;
531 border-radius: 4px;
532 background: var( --wpd-surface-elevated, #fff );
533 color: var( --wpd-fg, #1d2327 );
534 font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
535 font-size: 11px;
536 font-weight: 600;
537 line-height: 1;
538 white-space: nowrap;
539 }
540 #wpadminbar .desktop-mode-shortcuts-popover__plus {
541 font-size: 10px;
542 color: var( --wpd-fg-muted, #50575e );
543 }
544 #wpadminbar .desktop-mode-shortcuts-popover__description {
545 color: inherit;
546 }
547
548 /* Dock-style custom tooltip for icon-only admin-bar buttons.
549 admin-bar.js moves the native `title` to `data-desktop-tooltip`
550 so the OS delayed tooltip never competes with this one. Floats
551 below the button, dark pill with white text — visually
552 identical to the dock tooltip pattern. */
553 #wpadminbar .ab-item[ data-desktop-tooltip ] {
554 position: relative;
555 }
556 #wpadminbar .ab-item[ data-desktop-tooltip ]::after {
557 content: attr( data-desktop-tooltip );
558 position: absolute;
559 top: 100%;
560 left: 50%;
561 margin-top: 4px;
562 transform: translateX( -50% ) translateY( 4px );
563 padding: 6px 12px;
564 background: rgba( 0, 0, 0, 0.85 );
565 color: #fff;
566 font-size: 12px;
567 font-weight: 400;
568 line-height: 1.4;
569 white-space: nowrap;
570 border-radius: 6px;
571 pointer-events: none;
572 opacity: 0;
573 transition: opacity 0.15s ease, transform 0.15s ease;
574 z-index: 100000;
575 }
576 #wpadminbar .ab-item[ data-desktop-tooltip ]:hover::after,
577 #wpadminbar .ab-item[ data-desktop-tooltip ]:focus-visible::after {
578 opacity: 1;
579 transform: translateX( -50% ) translateY( 0 );
580 }
581 /* When the keyboard-shortcuts popover is open, suppress the
582 tooltip on its own anchor so the two never stack on top of
583 each other. */
584 #wpadminbar #wp-admin-bar-desktop-help.is-open .ab-item[ data-desktop-tooltip ]::after {
585 opacity: 0;
586 }
587
588 /* Arrange submenu — aligned visually with the <wpd-menu> component
589 (src/ui/components/wpd-menu/wpd-menu.styles.ts). The submenu
590 breaks out of the native dark-on-dark admin bar and paints
591 itself from the `--wpd-*` panel palette instead, so it matches
592 the rest of our UI and stays legible under any desktop theme.
593
594 Surface and text MUST come from the same palette. They used to
595 not: the background read `--desktop-mode-window-bg` (which a
596 theme sets) while the text read `--desktop-mode-text` (which
597 nothing has ever defined, so it always fell back to near-black).
598 Any dark theme therefore rendered near-black text on its own
599 near-black panel. Keep both sides on `--wpd-surface` /
600 `--wpd-fg` and that class of bug cannot come back.
601
602 Selectors prefixed with #wpadminbar to win the admin-bar
603 specificity without !important. */
604 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper {
605 background: var( --wpd-surface, var( --desktop-mode-window-bg, #fff ) );
606 padding: 4px;
607 min-width: 220px;
608 border: 1px solid var( --wpd-border, var( --desktop-mode-window-border, #c3c4c7 ) );
609 border-radius: 8px;
610 box-shadow: 0 8px 24px rgba( 0, 0, 0, 0.18 ),
611 0 2px 6px rgba( 0, 0, 0, 0.08 );
612 }
613 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu {
614 padding: 0;
615 background: transparent;
616 }
617 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu > li {
618 background: transparent;
619 }
620 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu > li > .ab-item {
621 display: flex;
622 align-items: center;
623 gap: 10px;
624 height: auto;
625 min-height: 32px;
626 padding: 6px 10px;
627 font-size: 13px;
628 line-height: 1.3;
629 color: var( --wpd-fg, #1d2327 );
630 background: transparent;
631 border-radius: 6px;
632 transition: background-color 0.12s ease, color 0.12s ease;
633 }
634 /* Hover + keyboard focus share the same tinted background. Text
635 stays dark so it stays legible on the light bg. `focus` wins
636 here instead of the native `.ab-item:focus { color: #72aee6 }`
637 thanks to the extra id in our selector chain. */
638 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu > li > .ab-item:hover,
639 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu > li > .ab-item:focus,
640 #wpadminbar.nojq #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu > li:hover > .ab-item {
641 background: var( --wpd-hover, rgba( 0, 0, 0, 0.06 ) );
642 color: var( --wpd-fg, #000 );
643 }
644 #wp-admin-bar-desktop-layout-snap .desktop-mode-layout-checkbox {
645 flex-shrink: 0;
646 display: inline-flex;
647 align-items: center;
648 justify-content: center;
649 width: 16px;
650 height: 16px;
651 font-size: 14px;
652 line-height: 1;
653 }
654 #wp-admin-bar-desktop-layout-snap[aria-checked="true"] .desktop-mode-layout-checkbox {
655 color: var( --wp-admin-theme-color, #2271b1 );
656 }
657 ';
658 wp_add_inline_style( 'admin-bar', $css );
659
660 // All PHP→JS values are emitted as JSON literals (never interpolated
661 // raw into the script body) so special characters, quotes, and
662 // unexpected shapes can't break the parser or be exploited.
663 // `active` must match the visual state shown on the toggle above —
664 // i.e. "currently viewing desktop mode." Using `desktop_mode_is_enabled()`
665 // alone would misclassify a classic-override request (meta = '1',
666 // URL carrying `desktop_mode_classic=1`) as active, causing the
667 // click handler to send `enabled=0` when the user actually wants
668 // to return to the shell.
669 wp_register_script(
670 'desktop-mode-admin-bar',
671 DESKTOP_MODE_URL . 'assets/js/admin-bar.js',
672 array( 'admin-bar' ),
673 DESKTOP_MODE_VERSION,
674 true
675 );
676 // Emit the config as a JSON literal via wp_add_inline_script (not
677 // wp_localize_script — the latter casts booleans to '' / '1', which
678 // breaks the click handler's `!! cfg.active` check). 'before' runs
679 // before admin-bar.js so the global is ready when the IIFE fires.
680 wp_add_inline_script(
681 'desktop-mode-admin-bar',
682 'var desktopModeAdminBar = ' . wp_json_encode(
683 array(
684 'nonce' => wp_create_nonce( 'save-desktop-mode' ),
685 'active' => desktop_mode_is_enabled() && ! desktop_mode_is_classic_request(),
686 'classicUrl' => esc_url_raw( admin_url() ),
687 'portalUrl' => esc_url_raw( desktop_mode_portal_url() ),
688 'ajaxUrl' => esc_url_raw( admin_url( 'admin-ajax.php' ) ),
689 'i18n' => array(
690 'enterFullscreen' => __( 'Fullscreen', 'desktop-mode' ),
691 'exitFullscreen' => __( 'Exit fullscreen', 'desktop-mode' ),
692 'enterTitle' => __( 'Enter fullscreen', 'desktop-mode' ),
693 'exitTitle' => __( 'Exit fullscreen', 'desktop-mode' ),
694 ),
695 /*
696 * Keyboard-shortcuts popover content. Translated once on
697 * the server and shipped to admin-bar.js, which renders
698 * the popover anchored under the keyboard button.
699 *
700 * `contextual` is a small table that distinguishes the
701 * three modes the shortcuts operate in (Outside Overview,
702 * Inside Overview, Show Desktop). `general` is a flat
703 * list for shortcuts whose behaviour doesn't shift by
704 * mode.
705 */
706 'shortcuts' => array(
707 'title' => __( 'Keyboard shortcuts', 'desktop-mode' ),
708 'contextual' => array(
709 'heading' => __( 'Desktops & Overview', 'desktop-mode' ),
710 'headers' => array(
711 'key' => __( 'Key', 'desktop-mode' ),
712 'outside' => __( 'Outside overview', 'desktop-mode' ),
713 'inside' => __( 'Inside overview', 'desktop-mode' ),
714 'showDesktop' => __( 'In Show Desktop', 'desktop-mode' ),
715 ),
716 'rows' => array(
717 array(
718 'keys' => array( '' ),
719 'outside' => __( 'Previous desktop (wraps)', 'desktop-mode' ),
720 'inside' => __( 'Previous desktop (grid + top-bar update)', 'desktop-mode' ),
721 'showDesktop' => __( 'Previous desktop', 'desktop-mode' ),
722 ),
723 array(
724 'keys' => array( '' ),
725 'outside' => __( 'Next desktop (wraps)', 'desktop-mode' ),
726 'inside' => __( 'Next desktop (grid + top-bar update)', 'desktop-mode' ),
727 'showDesktop' => __( 'Next desktop', 'desktop-mode' ),
728 ),
729 array(
730 'keys' => array( '' ),
731 'outside' => __( 'Enter Overview', 'desktop-mode' ),
732 'inside' => __( 'Exit Overview onto active desktop', 'desktop-mode' ),
733 'showDesktop' => __( 'Restore windows (exit Show Desktop)', 'desktop-mode' ),
734 ),
735 array(
736 'keys' => array( '' ),
737 'outside' => __( 'Toggle Show Desktop', 'desktop-mode' ),
738 'inside' => __( 'Exit Overview (no minimize)', 'desktop-mode' ),
739 'showDesktop' => __( 'Toggle Show Desktop', 'desktop-mode' ),
740 ),
741 array(
742 'keys' => array( 'Enter' ),
743 'note' => __( '(in overview)', 'desktop-mode' ),
744 'outside' => '',
745 'inside' => __( 'Commit current desktop, exit overview', 'desktop-mode' ),
746 'showDesktop' => '',
747 ),
748 ),
749 ),
750 'general' => array(
751 'heading' => __( 'Windows & palette', 'desktop-mode' ),
752 'items' => array(
753 array(
754 'keys' => array( '`' ),
755 'description' => __( 'Cycle to the next window on the active desktop.', 'desktop-mode' ),
756 ),
757 array(
758 'keys' => array( 'Shift', '`' ),
759 'description' => __( 'Cycle to the previous window on the active desktop.', 'desktop-mode' ),
760 ),
761 array(
762 'keys' => array( '⌘/Ctrl', 'K' ),
763 'description' => __( 'Open the command palette / Ask AI overlay.', 'desktop-mode' ),
764 ),
765 array(
766 'keys' => array( 'Esc' ),
767 'description' => __( 'Exit Overview (or Snap Overview) without changing window state.', 'desktop-mode' ),
768 ),
769 ),
770 ),
771 ),
772 )
773 ) . ';',
774 'before'
775 );
776 wp_enqueue_script( 'desktop-mode-admin-bar' );
777 }
778 add_action( 'admin_enqueue_scripts', 'desktop_mode_enqueue_toggle_assets' );
779
780