PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.9
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.9
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 0.8.6 All 33 releases
desktop-mode / includes / admin-bar.php

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

815 lines 30.2 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 // Inside a chromeless window the admin bar is suppressed outright
294 // (`show_admin_bar` + the `wp_admin_bar_render` removal in
295 // helpers.php), so `#wpadminbar` never reaches the DOM. Every byte
296 // below — the toggle bundle, its inline config, the node styling —
297 // would load and run against markup that does not exist. Measured
298 // on a live install: the toggle bundle alone was 17.7 KB, the
299 // largest single asset in the admin-bar family a window loaded for
300 // nothing. See `includes/render/chromeless-trim.php`, which drops
301 // the rest of that family (core's `admin-bar`, host masterbar
302 // extras) for the same reason.
303 if ( openstation_is_chromeless_request() ) {
304 return;
305 }
306
307 // The items are `display: flex`, never `inline-flex`. An
308 // inline-level box sits on a line box the <li> lays out at its
309 // 32px line-height and aligns on the baseline, so the line grows
310 // by the descender: the item became 37px inside a 32px bar. Under
311 // Core's float layout that overflow was invisible. On a host that
312 // lays the secondary group out as a flex row (WordPress.com's Debug
313 // Bar does, to order its own item first) every sibling stretched
314 // to the tallest one and the group's background painted 5px into
315 // the shell, under the windows' title bars. Block-level, the item
316 // is exactly the bar. `Tests_OpenStation_AdminBarDesktopToggle`
317 // pins it; `desktop.css` caps the group as well, for items we do
318 // not own.
319 $css = '
320 #wpadminbar #wp-admin-bar-os-toggle > .ab-item,
321 #wpadminbar #wp-admin-bar-desktop-layout-menu > .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: flex;
326 align-items: center;
327 gap: 6px;
328 }
329 #wpadminbar #wp-admin-bar-os-toggle .ab-icon,
330 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-icon,
331 #wpadminbar #wp-admin-bar-desktop-fullscreen .ab-icon,
332 #wpadminbar #wp-admin-bar-desktop-bug-report .ab-icon,
333 #wpadminbar #wp-admin-bar-desktop-help .ab-icon {
334 float: none;
335 margin: 0;
336 padding: 0;
337 display: inline-flex;
338 align-items: center;
339 justify-content: center;
340 width: 20px;
341 height: 20px;
342 }
343
344 #wp-admin-bar-os-toggle .ab-icon.dashicons,
345 #wp-admin-bar-desktop-layout-menu .ab-icon.dashicons {
346 font: normal 20px/1 dashicons;
347 -webkit-font-smoothing: antialiased;
348 -moz-osx-font-smoothing: grayscale;
349 }
350 #wp-admin-bar-os-toggle .ab-icon.dashicons::before {
351 content: "\f472";
352 top: 0;
353 position: static;
354 }
355 #wp-admin-bar-desktop-layout-menu .ab-icon.dashicons::before {
356 /* dashicons-grid-view */
357 content: "\f509";
358 top: 0;
359 position: static;
360 }
361 #wp-admin-bar-os-toggle.os-active .ab-icon.dashicons::before {
362 /* Inherit the admin-bar text color so the active state
363 matches the +New, Home, Comments dashicons. Previously
364 hardcoded #72aee6, which forced blue regardless of the
365 profile color scheme. */
366 color: inherit;
367 }
368 @media screen and (max-width: 782px) {
369 #wp-admin-bar-os-toggle .ab-label {
370 display: none;
371 }
372 }
373
374 /* Fullscreen admin-bar button — same dashicons rendering pattern
375 as the toggle. Icon swaps between fullscreen-alt (enter) and
376 fullscreen-exit-alt (exit) via the .is-fullscreen class set by
377 admin-bar.js on `fullscreenchange`. */
378 #wp-admin-bar-desktop-fullscreen .ab-icon.dashicons {
379 font: normal 20px/1 dashicons;
380 -webkit-font-smoothing: antialiased;
381 -moz-osx-font-smoothing: grayscale;
382 }
383 #wp-admin-bar-desktop-fullscreen .ab-icon.dashicons::before {
384 /* dashicons-fullscreen-alt */
385 content: "\f211";
386 top: 0;
387 position: static;
388 }
389 #wp-admin-bar-desktop-fullscreen.is-fullscreen .ab-icon.dashicons::before {
390 /* dashicons-fullscreen-exit-alt */
391 content: "\f212";
392 }
393 /* Defuse the implicit-drag-on-anchor click eater. WP renders
394 admin-bar items as <a href="#">, which is implicitly
395 draggable; tiny pointer jitter on click commits to a native
396 link-drag (the floating "Exit fullscreen / http://…#" ghost
397 the user sees) and the click handler never runs. admin-bar.js
398 also sets draggable="false" on the <a>; user-drag: none here
399 covers the WebKit path where the attribute alone is not
400 honoured reliably. */
401 #wp-admin-bar-desktop-fullscreen .ab-item,
402 #wp-admin-bar-desktop-fullscreen .ab-item * {
403 -webkit-user-drag: none;
404 user-drag: none;
405 -webkit-user-select: none;
406 user-select: none;
407 }
408 /* Hide admin-bar items that sit closest to the right corner
409 while in browser fullscreen. The corner is a reveal hot zone
410 the OS / browser reserve for menu-bar / window-controls,
411 where pointer events get intercepted before reaching the
412 page. Hiding these two shifts the Fullscreen toggle further
413 from the corner so its own click lands. Toggled by a body
414 class admin-bar.js sets on `fullscreenchange`. */
415 body.os-browser-fs #wp-admin-bar-os-toggle,
416 body.os-browser-fs #wp-admin-bar-desktop-bug-report {
417 display: none;
418 }
419
420 /* Bug Report admin-bar button — same dashicons rendering pattern
421 as the toggle. dashicons-buddicons-replies is a speech bubble
422 with stylized lines that reads as a comment / report glyph at
423 admin-bar size, while dashicons-bug is too literal and small. */
424 #wp-admin-bar-desktop-bug-report .ab-icon.dashicons {
425 font: normal 20px/1 dashicons;
426 -webkit-font-smoothing: antialiased;
427 -moz-osx-font-smoothing: grayscale;
428 }
429 #wp-admin-bar-desktop-bug-report .ab-icon.dashicons::before {
430 /* dashicons-buddicons-replies — same glyph as the dock tile so
431 the two surfaces (admin-bar + dock) match. */
432 content: "\f451";
433 top: 0;
434 position: static;
435 }
436
437 /* Keyboard shortcuts admin-bar button. Dashicons has no keyboard
438 glyph, so we paint a small inline SVG via CSS `mask` with
439 `background-color: currentColor` — that way the icon inherits
440 the admin-bar text color across every WP profile scheme
441 without us hardcoding a fill. */
442 #wp-admin-bar-desktop-help .ab-icon.os-keyboard-icon {
443 background-color: currentColor;
444 -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;
445 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;
446 }
447
448 /* Keyboard-shortcuts floating popover — anchored under the
449 keyboard button. Styled to match the Arrange submenu (light
450 bg on the otherwise-dark admin bar, soft shadow, rounded
451 corners). Built dynamically in admin-bar.js from translated
452 content shipped via `openStationAdminBar.shortcuts`. */
453 #wpadminbar #wp-admin-bar-desktop-help {
454 position: relative;
455 }
456 #wpadminbar .os-shortcuts-popover {
457 display: none;
458 position: absolute;
459 top: 100%;
460 right: 0;
461 margin-top: 0;
462 padding: 12px 14px;
463 min-width: 460px;
464 max-width: min( 90vw, 720px );
465 background: var( --os-ui-surface, var( --os-window-bg, #fff ) );
466 color: var( --os-ui-fg, #1d2327 );
467 border: 1px solid var( --os-ui-border, var( --os-window-border, #c3c4c7 ) );
468 border-radius: 8px;
469 box-shadow: 0 8px 24px rgba( 0, 0, 0, 0.18 ),
470 0 2px 6px rgba( 0, 0, 0, 0.08 );
471 z-index: 99999;
472 font-size: 12px;
473 line-height: 1.4;
474 text-align: left;
475 }
476 #wpadminbar .os-shortcuts-popover.is-open {
477 display: block;
478 }
479 #wpadminbar .os-shortcuts-popover__section + .os-shortcuts-popover__section {
480 margin-top: 12px;
481 }
482 #wpadminbar .os-shortcuts-popover__heading {
483 margin: 0 0 6px;
484 padding: 0;
485 font-size: 11px;
486 font-weight: 600;
487 letter-spacing: 0.04em;
488 text-transform: uppercase;
489 color: var( --os-ui-fg-muted, #50575e );
490 line-height: 1.2;
491 }
492 #wpadminbar .os-shortcuts-popover__table {
493 width: 100%;
494 border-collapse: collapse;
495 color: inherit;
496 }
497 #wpadminbar .os-shortcuts-popover__table th,
498 #wpadminbar .os-shortcuts-popover__table td {
499 padding: 6px 8px;
500 vertical-align: middle;
501 text-align: left;
502 border-bottom: 1px solid var( --os-ui-border, rgba( 0, 0, 0, 0.06 ) );
503 color: inherit;
504 font-weight: 400;
505 font-size: 12px;
506 line-height: 1.4;
507 }
508 #wpadminbar .os-shortcuts-popover__table th {
509 font-weight: 600;
510 font-size: 11px;
511 color: var( --os-ui-fg-muted, #50575e );
512 letter-spacing: 0.02em;
513 }
514 #wpadminbar .os-shortcuts-popover__table tbody tr:last-child td {
515 border-bottom: none;
516 }
517 #wpadminbar .os-shortcuts-popover__key-cell {
518 white-space: nowrap;
519 }
520 #wpadminbar .os-shortcuts-popover__note {
521 color: var( --os-ui-fg-muted, #50575e );
522 font-size: 11px;
523 }
524 #wpadminbar .os-shortcuts-popover__list {
525 margin: 0;
526 padding: 0;
527 list-style: none;
528 display: flex;
529 flex-direction: column;
530 gap: 4px;
531 }
532 #wpadminbar .os-shortcuts-popover__item {
533 display: grid;
534 grid-template-columns: 120px 1fr;
535 align-items: center;
536 gap: 12px;
537 padding: 4px 8px;
538 border-radius: 4px;
539 background: var( --os-ui-surface-sunken, rgba( 0, 0, 0, 0.03 ) );
540 }
541 #wpadminbar .os-shortcuts-popover__keys {
542 display: inline-flex;
543 align-items: center;
544 gap: 4px;
545 flex-wrap: wrap;
546 }
547 #wpadminbar .os-shortcuts-popover__kbd {
548 display: inline-flex;
549 align-items: center;
550 justify-content: center;
551 min-width: 22px;
552 height: 20px;
553 padding: 0 5px;
554 border: 1px solid var( --os-ui-border-strong, rgba( 0, 0, 0, 0.18 ) );
555 border-bottom-width: 2px;
556 border-radius: 4px;
557 background: var( --os-ui-surface-elevated, #fff );
558 color: var( --os-ui-fg, #1d2327 );
559 font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
560 font-size: 11px;
561 font-weight: 600;
562 line-height: 1;
563 white-space: nowrap;
564 }
565 #wpadminbar .os-shortcuts-popover__plus {
566 font-size: 10px;
567 color: var( --os-ui-fg-muted, #50575e );
568 }
569 #wpadminbar .os-shortcuts-popover__description {
570 color: inherit;
571 }
572
573 /* Dock-style custom tooltip for icon-only admin-bar buttons.
574 admin-bar.js moves the native `title` to `data-desktop-tooltip`
575 so the OS delayed tooltip never competes with this one. Floats
576 below the button, dark pill with white text — visually
577 identical to the dock tooltip pattern. */
578 #wpadminbar .ab-item[ data-desktop-tooltip ] {
579 position: relative;
580 }
581 #wpadminbar .ab-item[ data-desktop-tooltip ]::after {
582 content: attr( data-desktop-tooltip );
583 position: absolute;
584 top: 100%;
585 left: 50%;
586 margin-top: 4px;
587 transform: translateX( -50% ) translateY( 4px );
588 padding: 6px 12px;
589 background: rgba( 0, 0, 0, 0.85 );
590 color: #fff;
591 font-size: 12px;
592 font-weight: 400;
593 line-height: 1.4;
594 white-space: nowrap;
595 border-radius: 6px;
596 pointer-events: none;
597 opacity: 0;
598 transition: opacity 0.15s ease, transform 0.15s ease;
599 z-index: 100000;
600 }
601 #wpadminbar .ab-item[ data-desktop-tooltip ]:hover::after,
602 #wpadminbar .ab-item[ data-desktop-tooltip ]:focus-visible::after {
603 opacity: 1;
604 transform: translateX( -50% ) translateY( 0 );
605 }
606 /* When the keyboard-shortcuts popover is open, suppress the
607 tooltip on its own anchor so the two never stack on top of
608 each other. */
609 #wpadminbar #wp-admin-bar-desktop-help.is-open .ab-item[ data-desktop-tooltip ]::after {
610 opacity: 0;
611 }
612
613 /* Arrange submenu — aligned visually with the <os-menu> component
614 (src/ui/components/os-menu/os-menu.styles.ts). The submenu
615 breaks out of the native dark-on-dark admin bar and paints
616 itself from the `--os-ui-*` panel palette instead, so it matches
617 the rest of our UI and stays legible under any desktop theme.
618
619 Surface and text MUST come from the same palette. They used to
620 not: the background read `--os-window-bg` (which a
621 theme sets) while the text read `--os-text` (which
622 nothing has ever defined, so it always fell back to near-black).
623 Any dark theme therefore rendered near-black text on its own
624 near-black panel. Keep both sides on `--os-ui-surface` /
625 `--os-ui-fg` and that class of bug cannot come back.
626
627 Selectors prefixed with #wpadminbar to win the admin-bar
628 specificity without !important. */
629 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper {
630 background: var( --os-ui-surface, var( --os-window-bg, #fff ) );
631 padding: 4px;
632 min-width: 220px;
633 border: 1px solid var( --os-ui-border, var( --os-window-border, #c3c4c7 ) );
634 border-radius: 8px;
635 box-shadow: 0 8px 24px rgba( 0, 0, 0, 0.18 ),
636 0 2px 6px rgba( 0, 0, 0, 0.08 );
637 }
638 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu {
639 padding: 0;
640 background: transparent;
641 }
642 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu > li {
643 background: transparent;
644 }
645 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu > li > .ab-item {
646 display: flex;
647 align-items: center;
648 gap: 10px;
649 height: auto;
650 min-height: 32px;
651 padding: 6px 10px;
652 font-size: 13px;
653 line-height: 1.3;
654 color: var( --os-ui-fg, #1d2327 );
655 background: transparent;
656 border-radius: 6px;
657 transition: background-color 0.12s ease, color 0.12s ease;
658 }
659 /* Hover + keyboard focus share the same tinted background. Text
660 stays dark so it stays legible on the light bg. `focus` wins
661 here instead of the native `.ab-item:focus { color: #72aee6 }`
662 thanks to the extra id in our selector chain. */
663 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu > li > .ab-item:hover,
664 #wpadminbar #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu > li > .ab-item:focus,
665 #wpadminbar.nojq #wp-admin-bar-desktop-layout-menu .ab-sub-wrapper .ab-submenu > li:hover > .ab-item {
666 background: var( --os-ui-hover, rgba( 0, 0, 0, 0.06 ) );
667 color: var( --os-ui-fg, #000 );
668 }
669 #wp-admin-bar-desktop-layout-snap .os-layout-checkbox {
670 flex-shrink: 0;
671 display: inline-flex;
672 align-items: center;
673 justify-content: center;
674 width: 16px;
675 height: 16px;
676 font-size: 14px;
677 line-height: 1;
678 }
679 #wp-admin-bar-desktop-layout-snap[aria-checked="true"] .os-layout-checkbox {
680 color: var( --wp-admin-theme-color, #2271b1 );
681 }
682 ';
683 wp_add_inline_style( 'admin-bar', $css );
684
685 // All PHP→JS values are emitted as JSON literals (never interpolated
686 // raw into the script body) so special characters, quotes, and
687 // unexpected shapes can't break the parser or be exploited.
688 // `active` must match the visual state shown on the toggle above —
689 // i.e. "currently viewing OpenStation." Using `openstation_is_enabled()`
690 // alone would misclassify a classic-override request (meta = '1',
691 // URL carrying `desktop_mode_classic=1`) as active, causing the
692 // click handler to send `enabled=0` when the user actually wants
693 // to return to the shell.
694 wp_register_script(
695 'os-admin-bar',
696 OPENSTATION_URL . 'assets/js/admin-bar.js',
697 array( 'admin-bar' ),
698 OPENSTATION_VERSION,
699 true
700 );
701 // Emit the config as a JSON literal via wp_add_inline_script (not
702 // wp_localize_script — the latter casts booleans to '' / '1', which
703 // breaks the click handler's `!! cfg.active` check). 'before' runs
704 // before admin-bar.js so the global is ready when the IIFE fires.
705 wp_add_inline_script(
706 'os-admin-bar',
707 'var openStationAdminBar = ' . wp_json_encode(
708 array(
709 'nonce' => wp_create_nonce( 'save-openstation' ),
710 'active' => openstation_is_enabled() && ! openstation_is_classic_request(),
711 // `self_admin_url()`: switching off from the network
712 // admin returns there, not to the main site.
713 'classicUrl' => esc_url_raw( self_admin_url() ),
714 'portalUrl' => esc_url_raw( openstation_portal_url() ),
715 // Passed back on the toggle's AJAX call: the handler
716 // runs on `admin-ajax.php`, where `is_network_admin()`
717 // is always false.
718 'network' => is_network_admin(),
719 'ajaxUrl' => esc_url_raw( admin_url( 'admin-ajax.php' ) ),
720 'i18n' => array(
721 'enterFullscreen' => __( 'Fullscreen', 'desktop-mode' ),
722 'exitFullscreen' => __( 'Exit fullscreen', 'desktop-mode' ),
723 'enterTitle' => __( 'Enter fullscreen', 'desktop-mode' ),
724 'exitTitle' => __( 'Exit fullscreen', 'desktop-mode' ),
725 ),
726
727 /*
728 * Keyboard-shortcuts popover content. Translated once on
729 * the server and shipped to admin-bar.js, which renders
730 * the popover anchored under the keyboard button.
731 *
732 * `contextual` is a small table that distinguishes the
733 * three modes the shortcuts operate in (Outside Overview,
734 * Inside Overview, Show Desktop). `general` is a flat
735 * list for shortcuts whose behaviour doesn't shift by
736 * mode.
737 */
738 'shortcuts' => array(
739 'title' => __( 'Keyboard shortcuts', 'desktop-mode' ),
740 'contextual' => array(
741 'heading' => __( 'Desktops & Overview', 'desktop-mode' ),
742 'headers' => array(
743 'key' => __( 'Key', 'desktop-mode' ),
744 'outside' => __( 'Outside overview', 'desktop-mode' ),
745 'inside' => __( 'Inside overview', 'desktop-mode' ),
746 'showDesktop' => __( 'In Show Desktop', 'desktop-mode' ),
747 ),
748 'rows' => array(
749 array(
750 'keys' => array( '' ),
751 'outside' => __( 'Previous desktop (wraps)', 'desktop-mode' ),
752 'inside' => __( 'Previous desktop (grid + top-bar update)', 'desktop-mode' ),
753 'showDesktop' => __( 'Previous desktop', 'desktop-mode' ),
754 ),
755 array(
756 'keys' => array( '' ),
757 'outside' => __( 'Next desktop (wraps)', 'desktop-mode' ),
758 'inside' => __( 'Next desktop (grid + top-bar update)', 'desktop-mode' ),
759 'showDesktop' => __( 'Next desktop', 'desktop-mode' ),
760 ),
761 array(
762 'keys' => array( '' ),
763 'outside' => __( 'Enter Overview', 'desktop-mode' ),
764 'inside' => __( 'Exit Overview onto active desktop', 'desktop-mode' ),
765 'showDesktop' => __( 'Restore windows (exit Show Desktop)', 'desktop-mode' ),
766 ),
767 array(
768 'keys' => array( '' ),
769 'outside' => __( 'Toggle Show Desktop', 'desktop-mode' ),
770 'inside' => __( 'Exit Overview (no minimize)', 'desktop-mode' ),
771 'showDesktop' => __( 'Toggle Show Desktop', 'desktop-mode' ),
772 ),
773 array(
774 'keys' => array( 'Enter' ),
775 'note' => __( '(in overview)', 'desktop-mode' ),
776 'outside' => '',
777 'inside' => __( 'Commit current desktop, exit overview', 'desktop-mode' ),
778 'showDesktop' => '',
779 ),
780 ),
781 ),
782 'general' => array(
783 'heading' => __( 'Windows & palette', 'desktop-mode' ),
784 'items' => array(
785 array(
786 'keys' => array( '`' ),
787 'description' => __( 'Cycle to the next window on the active desktop.', 'desktop-mode' ),
788 ),
789 array(
790 'keys' => array( 'Shift', '`' ),
791 'description' => __( 'Cycle to the previous window on the active desktop.', 'desktop-mode' ),
792 ),
793 array(
794 'keys' => array( '⌘/Ctrl', 'K' ),
795 'description' => __( 'Open the command palette / Ask AI overlay.', 'desktop-mode' ),
796 ),
797 array(
798 'keys' => array( '⌥/Alt', '⌘/Ctrl', 'W' ),
799 'description' => __( 'Close every open window on the current desktop (asks first).', 'desktop-mode' ),
800 ),
801 array(
802 'keys' => array( 'Esc' ),
803 'description' => __( 'Exit Overview (or Snap Overview) without changing window state.', 'desktop-mode' ),
804 ),
805 ),
806 ),
807 ),
808 )
809 ) . ';',
810 'before'
811 );
812 wp_enqueue_script( 'os-admin-bar' );
813 }
814 add_action( 'admin_enqueue_scripts', 'openstation_enqueue_toggle_assets' );
815