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

862 lines 31.7 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 // AI Assistant trigger — shown when desktop mode is active AND the
236 // current user has AI features configured. Clicking (or pressing
237 // Cmd+K anywhere) opens the spotlight-style AI overlay.
238 // Rendered whenever the AI APIs are available; its visibility is driven by
239 // the `desktop-mode-ai-enabled` body class (set server-side in
240 // body-classes.php, toggled live by the shell when the user flips the
241 // assistant toggle) so no reload is needed to show/hide it.
242 if ( $is_active
243 && function_exists( 'desktop_mode_ai_is_available' ) && desktop_mode_ai_is_available() ) {
244 // Use dashicons-admin-comments (speech bubble) — same rendering
245 // path as the toggle + arrange buttons, no SVG / HTML parsing
246 // issues in the admin-bar context. The ⌘K badge is added via
247 // a CSS ::after on the label so it never touches the DOM.
248 $wp_admin_bar->add_node(
249 array(
250 'parent' => 'top-secondary',
251 'id' => 'desktop-ai-assistant',
252 'title' => '<span class="ab-icon dashicons dashicons-admin-comments" aria-hidden="true"></span>'
253 . '<span class="ab-label">' . esc_html__( 'Ask AI', 'desktop-mode' ) . '</span>',
254 'href' => '#',
255 'meta' => array(
256 'class' => 'desktop-ai-btn',
257 'title' => __( 'Open AI Assistant (Cmd+K)', 'desktop-mode' ),
258 'tabindex' => 0,
259 ),
260 )
261 );
262 }
263
264 // "Keyboard shortcuts" trigger — shown only when desktop mode is
265 // active. Clicking toggles the keyboard-shortcuts popover wired by
266 // assets/js/admin-bar.js (wireShortcutsPopover); the popover content
267 // is translated server-side and shipped via the `shortcuts` key of
268 // the desktopModeAdminBar config blob below.
269 if ( $is_active ) {
270 $wp_admin_bar->add_node(
271 array(
272 'parent' => 'top-secondary',
273 'id' => 'desktop-help',
274 'title' => '<span class="ab-icon desktop-mode-keyboard-icon" aria-hidden="true"></span>',
275 'href' => '#',
276 'meta' => array(
277 'class' => 'desktop-help-btn',
278 'title' => __( 'Keyboard shortcuts', 'desktop-mode' ),
279 'tabindex' => 0,
280 ),
281 )
282 );
283 }
284
285 // "Report a bug" trigger — shown only when desktop mode is active.
286 // Clicking dispatches a `desktop-mode-open-bug-report` document
287 // CustomEvent that the shell listens for and answers by opening the
288 // Bug Report native window. PHP doesn't know the JS side exists; the
289 // event lets us add the button without coupling to any specific
290 // shell module. Anchored to the far right of the secondary bar so
291 // the meta/help cluster (Keyboard shortcuts + Report a bug) sits
292 // next to the user identity menu — matches the convention used by
293 // most SaaS products (Help / "?" at the user-menu corner).
294 if ( $is_active ) {
295 $wp_admin_bar->add_node(
296 array(
297 'parent' => 'top-secondary',
298 'id' => 'desktop-bug-report',
299 'title' => '<span class="ab-icon dashicons dashicons-buddicons-replies" aria-hidden="true"></span>',
300 'href' => '#',
301 'meta' => array(
302 'class' => 'desktop-bug-report-btn',
303 'title' => __( 'Open the Bug Report window', 'desktop-mode' ),
304 'tabindex' => 0,
305 ),
306 )
307 );
308 }
309 }
310 add_action( 'admin_bar_menu', 'desktop_mode_admin_bar_toggle', 190 );
311
312 /**
313 * Enqueues the CSS and JS for the desktop mode toggle.
314 *
315 * The CSS is inline, attached to the `admin-bar` style handle so it always
316 * ships with the admin bar itself — no matter which admin screen is showing.
317 * The JS is the external assets/js/admin-bar.js bundle, registered as
318 * `desktop-mode-admin-bar` with `admin-bar` as a dependency; its config is
319 * emitted as an inline JSON literal `before` the script.
320 *
321 * @since 0.1.0
322 */
323 function desktop_mode_enqueue_toggle_assets() {
324 if ( ! is_admin() || ! is_user_logged_in() ) {
325 return;
326 }
327
328 $css = '
329 #wpadminbar #wp-admin-bar-desktop-mode-toggle > .ab-item,
330 #wpadminbar #wp-admin-bar-desktop-layout-menu > .ab-item,
331 #wpadminbar #wp-admin-bar-desktop-ai-assistant > .ab-item,
332 #wpadminbar #wp-admin-bar-desktop-fullscreen > .ab-item,
333 #wpadminbar #wp-admin-bar-desktop-bug-report > .ab-item,
334 #wpadminbar #wp-admin-bar-desktop-help > .ab-item {
335 display: inline-flex;
336 align-items: center;
337 gap: 6px;
338 }
339 #wpadminbar #wp-admin-bar-desktop-mode-toggle .ab-icon,
340 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-icon,
341 #wpadminbar #wp-admin-bar-desktop-ai-assistant .ab-icon,
342 #wpadminbar #wp-admin-bar-desktop-fullscreen .ab-icon,
343 #wpadminbar #wp-admin-bar-desktop-bug-report .ab-icon,
344 #wpadminbar #wp-admin-bar-desktop-help .ab-icon {
345 float: none;
346 margin: 0;
347 padding: 0;
348 display: inline-flex;
349 align-items: center;
350 justify-content: center;
351 width: 20px;
352 height: 20px;
353 }
354
355 /* Ask AI button shows only when the per-user assistant toggle is on.
356 The body class is set server-side and toggled live by the shell, so
357 enabling/disabling the assistant needs no reload. */
358 body:not( .desktop-mode-ai-enabled ) #wpadminbar #wp-admin-bar-desktop-ai-assistant {
359 display: none;
360 }
361
362 #wp-admin-bar-desktop-mode-toggle .ab-icon.dashicons,
363 #wp-admin-bar-desktop-layout-menu .ab-icon.dashicons {
364 font: normal 20px/1 dashicons;
365 -webkit-font-smoothing: antialiased;
366 -moz-osx-font-smoothing: grayscale;
367 }
368 #wp-admin-bar-desktop-mode-toggle .ab-icon.dashicons::before {
369 content: "\f472";
370 top: 0;
371 position: static;
372 }
373 #wp-admin-bar-desktop-layout-menu .ab-icon.dashicons::before {
374 /* dashicons-grid-view */
375 content: "\f509";
376 top: 0;
377 position: static;
378 }
379 #wp-admin-bar-desktop-mode-toggle.desktop-mode-active .ab-icon.dashicons::before {
380 /* Inherit the admin-bar text color so the active state
381 matches the +New, Home, Comments dashicons. Previously
382 hardcoded #72aee6, which forced blue regardless of the
383 profile color scheme. */
384 color: inherit;
385 }
386 @media screen and (max-width: 782px) {
387 #wp-admin-bar-desktop-mode-toggle .ab-label {
388 display: none;
389 }
390 }
391
392 /* AI Assistant admin-bar button — same icon/label pattern as the
393 desktop-mode-toggle; ⌘K badge added via CSS ::after so we keep
394 the title HTML clean and avoid admin-bar sanitisation edge-cases. */
395 #wp-admin-bar-desktop-ai-assistant .ab-icon.dashicons,
396 #wp-admin-bar-desktop-ai-assistant .ab-icon.dashicons {
397 font: normal 20px/1 dashicons;
398 -webkit-font-smoothing: antialiased;
399 -moz-osx-font-smoothing: grayscale;
400 }
401 #wp-admin-bar-desktop-ai-assistant .ab-icon.dashicons::before {
402 content: "\f101";
403 top: 0;
404 position: static;
405 /* See note on desktop-mode-toggle above — inherit so the
406 icon matches the rest of the admin-bar dashicons
407 across all WP profile color schemes. */
408 color: inherit;
409 }
410 /* ⌘K badge rendered purely in CSS to the right of the label.
411 inline-flex + align-items:center centers the glyph inside
412 its own padding box; a 1px upward translate compensates
413 for the optical sag from the admin-bar label baseline. */
414 #wpadminbar #wp-admin-bar-desktop-ai-assistant .ab-label::after {
415 content: "\2318K";
416 display: inline-flex;
417 align-items: center;
418 justify-content: center;
419 margin-inline-start: 5px;
420 font-size: 10px;
421 line-height: 1;
422 padding: 2px 5px;
423 background: rgba( 255, 255, 255, 0.1 );
424 border: 1px solid rgba( 255, 255, 255, 0.18 );
425 border-radius: 3px;
426 color: rgba( 255, 255, 255, 0.55 );
427 vertical-align: middle;
428 font-weight: 400;
429 letter-spacing: 0;
430 position: relative;
431 top: -1px;
432 }
433 @media screen and (max-width: 782px) {
434 #wp-admin-bar-desktop-ai-assistant .ab-label {
435 display: none;
436 }
437 }
438
439 /* Fullscreen admin-bar button — same dashicons rendering pattern
440 as the toggle. Icon swaps between fullscreen-alt (enter) and
441 fullscreen-exit-alt (exit) via the .is-fullscreen class set by
442 admin-bar.js on `fullscreenchange`. */
443 #wp-admin-bar-desktop-fullscreen .ab-icon.dashicons {
444 font: normal 20px/1 dashicons;
445 -webkit-font-smoothing: antialiased;
446 -moz-osx-font-smoothing: grayscale;
447 }
448 #wp-admin-bar-desktop-fullscreen .ab-icon.dashicons::before {
449 /* dashicons-fullscreen-alt */
450 content: "\f211";
451 top: 0;
452 position: static;
453 }
454 #wp-admin-bar-desktop-fullscreen.is-fullscreen .ab-icon.dashicons::before {
455 /* dashicons-fullscreen-exit-alt */
456 content: "\f212";
457 }
458 /* Defuse the implicit-drag-on-anchor click eater. WP renders
459 admin-bar items as <a href="#">, which is implicitly
460 draggable; tiny pointer jitter on click commits to a native
461 link-drag (the floating "Exit fullscreen / http://…#" ghost
462 the user sees) and the click handler never runs. admin-bar.js
463 also sets draggable="false" on the <a>; user-drag: none here
464 covers the WebKit path where the attribute alone is not
465 honoured reliably. */
466 #wp-admin-bar-desktop-fullscreen .ab-item,
467 #wp-admin-bar-desktop-fullscreen .ab-item * {
468 -webkit-user-drag: none;
469 user-drag: none;
470 -webkit-user-select: none;
471 user-select: none;
472 }
473 /* Hide admin-bar items that sit closest to the right corner
474 while in browser fullscreen. The corner is a reveal hot zone
475 the OS / browser reserve for menu-bar / window-controls,
476 where pointer events get intercepted before reaching the
477 page. Hiding these two shifts the Fullscreen toggle further
478 from the corner so its own click lands. Toggled by a body
479 class admin-bar.js sets on `fullscreenchange`. */
480 body.desktop-mode-browser-fs #wp-admin-bar-desktop-mode-toggle,
481 body.desktop-mode-browser-fs #wp-admin-bar-desktop-bug-report {
482 display: none;
483 }
484
485 /* Bug Report admin-bar button — same dashicons rendering pattern
486 as the toggle. dashicons-buddicons-replies is a speech bubble
487 with stylized lines that reads as a comment / report glyph at
488 admin-bar size, while dashicons-bug is too literal and small. */
489 #wp-admin-bar-desktop-bug-report .ab-icon.dashicons {
490 font: normal 20px/1 dashicons;
491 -webkit-font-smoothing: antialiased;
492 -moz-osx-font-smoothing: grayscale;
493 }
494 #wp-admin-bar-desktop-bug-report .ab-icon.dashicons::before {
495 /* dashicons-buddicons-replies — same glyph as the dock tile so
496 the two surfaces (admin-bar + dock) match. */
497 content: "\f451";
498 top: 0;
499 position: static;
500 }
501
502 /* Keyboard shortcuts admin-bar button. Dashicons has no keyboard
503 glyph, so we paint a small inline SVG via CSS `mask` with
504 `background-color: currentColor` — that way the icon inherits
505 the admin-bar text color across every WP profile scheme
506 without us hardcoding a fill. */
507 #wp-admin-bar-desktop-help .ab-icon.desktop-mode-keyboard-icon {
508 background-color: currentColor;
509 -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;
510 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;
511 }
512
513 /* Keyboard-shortcuts floating popover — anchored under the
514 keyboard button. Styled to match the Arrange submenu (light
515 bg on the otherwise-dark admin bar, soft shadow, rounded
516 corners). Built dynamically in admin-bar.js from translated
517 content shipped via `desktopModeAdminBar.shortcuts`. */
518 #wpadminbar #wp-admin-bar-desktop-help {
519 position: relative;
520 }
521 #wpadminbar .desktop-mode-shortcuts-popover {
522 display: none;
523 position: absolute;
524 top: 100%;
525 right: 0;
526 margin-top: 0;
527 padding: 12px 14px;
528 min-width: 460px;
529 max-width: min( 90vw, 720px );
530 background: var( --desktop-mode-window-bg, #fff );
531 color: var( --desktop-mode-text, #1d2327 );
532 border: 1px solid var( --desktop-mode-window-border, #c3c4c7 );
533 border-radius: 8px;
534 box-shadow: 0 8px 24px rgba( 0, 0, 0, 0.18 ),
535 0 2px 6px rgba( 0, 0, 0, 0.08 );
536 z-index: 99999;
537 font-size: 12px;
538 line-height: 1.4;
539 text-align: left;
540 }
541 #wpadminbar .desktop-mode-shortcuts-popover.is-open {
542 display: block;
543 }
544 #wpadminbar .desktop-mode-shortcuts-popover__section + .desktop-mode-shortcuts-popover__section {
545 margin-top: 12px;
546 }
547 #wpadminbar .desktop-mode-shortcuts-popover__heading {
548 margin: 0 0 6px;
549 padding: 0;
550 font-size: 11px;
551 font-weight: 600;
552 letter-spacing: 0.04em;
553 text-transform: uppercase;
554 color: var( --desktop-mode-muted-fg, #50575e );
555 line-height: 1.2;
556 }
557 #wpadminbar .desktop-mode-shortcuts-popover__table {
558 width: 100%;
559 border-collapse: collapse;
560 color: inherit;
561 }
562 #wpadminbar .desktop-mode-shortcuts-popover__table th,
563 #wpadminbar .desktop-mode-shortcuts-popover__table td {
564 padding: 6px 8px;
565 vertical-align: middle;
566 text-align: left;
567 border-bottom: 1px solid rgba( 0, 0, 0, 0.06 );
568 color: inherit;
569 font-weight: 400;
570 font-size: 12px;
571 line-height: 1.4;
572 }
573 #wpadminbar .desktop-mode-shortcuts-popover__table th {
574 font-weight: 600;
575 font-size: 11px;
576 color: var( --desktop-mode-muted-fg, #50575e );
577 letter-spacing: 0.02em;
578 }
579 #wpadminbar .desktop-mode-shortcuts-popover__table tbody tr:last-child td {
580 border-bottom: none;
581 }
582 #wpadminbar .desktop-mode-shortcuts-popover__key-cell {
583 white-space: nowrap;
584 }
585 #wpadminbar .desktop-mode-shortcuts-popover__note {
586 color: var( --desktop-mode-muted-fg, #50575e );
587 font-size: 11px;
588 }
589 #wpadminbar .desktop-mode-shortcuts-popover__list {
590 margin: 0;
591 padding: 0;
592 list-style: none;
593 display: flex;
594 flex-direction: column;
595 gap: 4px;
596 }
597 #wpadminbar .desktop-mode-shortcuts-popover__item {
598 display: grid;
599 grid-template-columns: 120px 1fr;
600 align-items: center;
601 gap: 12px;
602 padding: 4px 8px;
603 border-radius: 4px;
604 background: rgba( 0, 0, 0, 0.03 );
605 }
606 #wpadminbar .desktop-mode-shortcuts-popover__keys {
607 display: inline-flex;
608 align-items: center;
609 gap: 4px;
610 flex-wrap: wrap;
611 }
612 #wpadminbar .desktop-mode-shortcuts-popover__kbd {
613 display: inline-flex;
614 align-items: center;
615 justify-content: center;
616 min-width: 22px;
617 height: 20px;
618 padding: 0 5px;
619 border: 1px solid rgba( 0, 0, 0, 0.18 );
620 border-bottom-width: 2px;
621 border-radius: 4px;
622 background: #fff;
623 color: var( --desktop-mode-text, #1d2327 );
624 font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
625 font-size: 11px;
626 font-weight: 600;
627 line-height: 1;
628 white-space: nowrap;
629 }
630 #wpadminbar .desktop-mode-shortcuts-popover__plus {
631 font-size: 10px;
632 color: var( --desktop-mode-muted-fg, #50575e );
633 }
634 #wpadminbar .desktop-mode-shortcuts-popover__description {
635 color: inherit;
636 }
637
638 /* Dock-style custom tooltip for icon-only admin-bar buttons.
639 admin-bar.js moves the native `title` to `data-desktop-tooltip`
640 so the OS delayed tooltip never competes with this one. Floats
641 below the button, dark pill with white text — visually
642 identical to the dock tooltip pattern. */
643 #wpadminbar .ab-item[ data-desktop-tooltip ] {
644 position: relative;
645 }
646 #wpadminbar .ab-item[ data-desktop-tooltip ]::after {
647 content: attr( data-desktop-tooltip );
648 position: absolute;
649 top: 100%;
650 left: 50%;
651 margin-top: 4px;
652 transform: translateX( -50% ) translateY( 4px );
653 padding: 6px 12px;
654 background: rgba( 0, 0, 0, 0.85 );
655 color: #fff;
656 font-size: 12px;
657 font-weight: 400;
658 line-height: 1.4;
659 white-space: nowrap;
660 border-radius: 6px;
661 pointer-events: none;
662 opacity: 0;
663 transition: opacity 0.15s ease, transform 0.15s ease;
664 z-index: 100000;
665 }
666 #wpadminbar .ab-item[ data-desktop-tooltip ]:hover::after,
667 #wpadminbar .ab-item[ data-desktop-tooltip ]:focus-visible::after {
668 opacity: 1;
669 transform: translateX( -50% ) translateY( 0 );
670 }
671 /* When the keyboard-shortcuts popover is open, suppress the
672 tooltip on its own anchor so the two never stack on top of
673 each other. */
674 #wpadminbar #wp-admin-bar-desktop-help.is-open .ab-item[ data-desktop-tooltip ]::after {
675 opacity: 0;
676 }
677
678 /* Arrange submenu — aligned visually with the <wpd-menu> component
679 (src/ui/components/wpd-menu/wpd-menu.styles.ts). The submenu
680 flips from the native dark-on-dark admin bar to a light theme
681 (white bg, dark text) so it matches the rest of our UI AND so
682 the hover state stays legible (a light tint on a dark bg
683 would render as invisible overlap with native admin-bar hover
684 colors). Selectors prefixed with #wpadminbar to win the
685 admin-bar specificity without !important. */
686 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper {
687 background: var( --desktop-mode-window-bg, #fff );
688 padding: 4px;
689 min-width: 220px;
690 border: 1px solid var( --desktop-mode-window-border, #c3c4c7 );
691 border-radius: 8px;
692 box-shadow: 0 8px 24px rgba( 0, 0, 0, 0.18 ),
693 0 2px 6px rgba( 0, 0, 0, 0.08 );
694 }
695 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu {
696 padding: 0;
697 background: transparent;
698 }
699 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu > li {
700 background: transparent;
701 }
702 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu > li > .ab-item {
703 display: flex;
704 align-items: center;
705 gap: 10px;
706 height: auto;
707 min-height: 32px;
708 padding: 6px 10px;
709 font-size: 13px;
710 line-height: 1.3;
711 color: var( --desktop-mode-text, #1d2327 );
712 background: transparent;
713 border-radius: 6px;
714 transition: background-color 0.12s ease, color 0.12s ease;
715 }
716 /* Hover + keyboard focus share the same tinted background. Text
717 stays dark so it stays legible on the light bg. `focus` wins
718 here instead of the native `.ab-item:focus { color: #72aee6 }`
719 thanks to the extra id in our selector chain. */
720 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu > li > .ab-item:hover,
721 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu > li > .ab-item:focus,
722 #wpadminbar.nojq #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu > li:hover > .ab-item {
723 background: rgba( 0, 0, 0, 0.06 );
724 color: #000;
725 }
726 #wp-admin-bar-desktop-layout-snap .desktop-mode-layout-checkbox {
727 flex-shrink: 0;
728 display: inline-flex;
729 align-items: center;
730 justify-content: center;
731 width: 16px;
732 height: 16px;
733 font-size: 14px;
734 line-height: 1;
735 }
736 #wp-admin-bar-desktop-layout-snap[aria-checked="true"] .desktop-mode-layout-checkbox {
737 color: var( --wp-admin-theme-color, #2271b1 );
738 }
739 ';
740 wp_add_inline_style( 'admin-bar', $css );
741
742 // All PHP→JS values are emitted as JSON literals (never interpolated
743 // raw into the script body) so special characters, quotes, and
744 // unexpected shapes can't break the parser or be exploited.
745 // `active` must match the visual state shown on the toggle above —
746 // i.e. "currently viewing desktop mode." Using `desktop_mode_is_enabled()`
747 // alone would misclassify a classic-override request (meta = '1',
748 // URL carrying `desktop_mode_classic=1`) as active, causing the
749 // click handler to send `enabled=0` when the user actually wants
750 // to return to the shell.
751 wp_register_script(
752 'desktop-mode-admin-bar',
753 DESKTOP_MODE_URL . 'assets/js/admin-bar.js',
754 array( 'admin-bar' ),
755 DESKTOP_MODE_VERSION,
756 true
757 );
758 // Emit the config as a JSON literal via wp_add_inline_script (not
759 // wp_localize_script — the latter casts booleans to '' / '1', which
760 // breaks the click handler's `!! cfg.active` check). 'before' runs
761 // before admin-bar.js so the global is ready when the IIFE fires.
762 wp_add_inline_script(
763 'desktop-mode-admin-bar',
764 'var desktopModeAdminBar = ' . wp_json_encode(
765 array(
766 'nonce' => wp_create_nonce( 'save-desktop-mode' ),
767 'active' => desktop_mode_is_enabled() && ! desktop_mode_is_classic_request(),
768 'classicUrl' => esc_url_raw( admin_url() ),
769 'portalUrl' => esc_url_raw( desktop_mode_portal_url() ),
770 'ajaxUrl' => esc_url_raw( admin_url( 'admin-ajax.php' ) ),
771 'i18n' => array(
772 'enterFullscreen' => __( 'Fullscreen', 'desktop-mode' ),
773 'exitFullscreen' => __( 'Exit fullscreen', 'desktop-mode' ),
774 'enterTitle' => __( 'Enter fullscreen', 'desktop-mode' ),
775 'exitTitle' => __( 'Exit fullscreen', 'desktop-mode' ),
776 ),
777 /*
778 * Keyboard-shortcuts popover content. Translated once on
779 * the server and shipped to admin-bar.js, which renders
780 * the popover anchored under the keyboard button.
781 *
782 * `contextual` is a small table that distinguishes the
783 * three modes the shortcuts operate in (Outside Overview,
784 * Inside Overview, Show Desktop). `general` is a flat
785 * list for shortcuts whose behaviour doesn't shift by
786 * mode.
787 */
788 'shortcuts' => array(
789 'title' => __( 'Keyboard shortcuts', 'desktop-mode' ),
790 'contextual' => array(
791 'heading' => __( 'Desktops & Overview', 'desktop-mode' ),
792 'headers' => array(
793 'key' => __( 'Key', 'desktop-mode' ),
794 'outside' => __( 'Outside overview', 'desktop-mode' ),
795 'inside' => __( 'Inside overview', 'desktop-mode' ),
796 'showDesktop' => __( 'In Show Desktop', 'desktop-mode' ),
797 ),
798 'rows' => array(
799 array(
800 'keys' => array( '' ),
801 'outside' => __( 'Previous desktop (wraps)', 'desktop-mode' ),
802 'inside' => __( 'Previous desktop (grid + top-bar update)', 'desktop-mode' ),
803 'showDesktop' => __( 'Previous desktop', 'desktop-mode' ),
804 ),
805 array(
806 'keys' => array( '' ),
807 'outside' => __( 'Next desktop (wraps)', 'desktop-mode' ),
808 'inside' => __( 'Next desktop (grid + top-bar update)', 'desktop-mode' ),
809 'showDesktop' => __( 'Next desktop', 'desktop-mode' ),
810 ),
811 array(
812 'keys' => array( '' ),
813 'outside' => __( 'Enter Overview', 'desktop-mode' ),
814 'inside' => __( 'Exit Overview onto active desktop', 'desktop-mode' ),
815 'showDesktop' => __( 'Restore windows (exit Show Desktop)', 'desktop-mode' ),
816 ),
817 array(
818 'keys' => array( '' ),
819 'outside' => __( 'Toggle Show Desktop', 'desktop-mode' ),
820 'inside' => __( 'Exit Overview (no minimize)', 'desktop-mode' ),
821 'showDesktop' => __( 'Toggle Show Desktop', 'desktop-mode' ),
822 ),
823 array(
824 'keys' => array( 'Enter' ),
825 'note' => __( '(in overview)', 'desktop-mode' ),
826 'outside' => '',
827 'inside' => __( 'Commit current desktop, exit overview', 'desktop-mode' ),
828 'showDesktop' => '',
829 ),
830 ),
831 ),
832 'general' => array(
833 'heading' => __( 'Windows & palette', 'desktop-mode' ),
834 'items' => array(
835 array(
836 'keys' => array( '`' ),
837 'description' => __( 'Cycle to the next window on the active desktop.', 'desktop-mode' ),
838 ),
839 array(
840 'keys' => array( 'Shift', '`' ),
841 'description' => __( 'Cycle to the previous window on the active desktop.', 'desktop-mode' ),
842 ),
843 array(
844 'keys' => array( '⌘/Ctrl', 'K' ),
845 'description' => __( 'Open the command palette / Ask AI overlay.', 'desktop-mode' ),
846 ),
847 array(
848 'keys' => array( 'Esc' ),
849 'description' => __( 'Exit Overview (or Snap Overview) without changing window state.', 'desktop-mode' ),
850 ),
851 ),
852 ),
853 ),
854 )
855 ) . ';',
856 'before'
857 );
858 wp_enqueue_script( 'desktop-mode-admin-bar' );
859 }
860 add_action( 'admin_enqueue_scripts', 'desktop_mode_enqueue_toggle_assets' );
861
862