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

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