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

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