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 / includes / admin-bar.php

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

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