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

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