PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.11
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.11
1.1.11 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 All 35 releases
← All changes | includes/admin-bar.php +79 -626 1.1.10 → 1.1.11 View file →
@@ -4,8 +4,19 @@
4 4 *
5 5 * Adds the "Switch to OpenStation" button to the admin bar's top-right
6 6 * area and wires its click handler to the save-openstation AJAX endpoint.
7 7 *
8 + * The button is the way INTO the shell and only shows in classic admin.
9 + * Inside the shell the admin bar carries no OpenStation nodes at all:
10 + * everything the desktop offers is reachable from the dock, and the way
11 + * back out is the locked "Exit OpenStation" tile.
12 + *
13 + * This file also publishes the `openStationAdminBar` config global,
14 + * which the shell reads for the exit tile's AJAX call
15 + * (`src/exit-openstation.ts`) and the keyboard-shortcuts window
16 + * (`src/shortcuts.ts`). It is emitted on every admin screen, with or
17 + * without the toggle node.
18 + *
8 19 * @package OpenStation
9 20 */
10 21
11 22 defined( 'ABSPATH' ) || exit;
@@ -12,10 +23,12 @@
12 23
13 24 /**
14 25 * Adds the OpenStation toggle to the admin bar.
15 26 *
16 - * Allows users to switch between classic admin and OpenStation,
17 - * which renders admin screens in draggable, resizable windows.
27 + * Only in classic admin. Once the user is viewing the shell the node
28 + * would be a second way to do what the "Exit OpenStation" dock tile
29 + * already does, and an admin bar carrying shell controls reads as the
30 + * shell's own toolbar when it is not one.
18 31 *
19 32 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
20 33 */
21 34 function openstation_admin_bar_toggle( $wp_admin_bar ) {
@@ -22,23 +35,19 @@
22 35 if ( ! is_admin() || ! is_user_logged_in() ) {
23 36 return;
24 37 }
25 38
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' );
39 + // "Active" means "the user is *currently viewing* OpenStation", not
40 + // "the preference is enabled in user meta." These diverge on requests
41 + // carrying the per-request classic override (`?desktop_mode_classic=1`):
42 + // the user's meta may be '1' but the page they're looking at right now
43 + // is classic admin, and it needs the way back in.
44 + if ( openstation_is_enabled() && ! openstation_is_classic_request() ) {
45 + return;
46 + }
40 47
48 + $label = __( 'Switch to OpenStation', 'desktop-mode' );
49 +
41 50 $wp_admin_bar->add_node(
42 51 array(
43 52 'parent' => 'top-secondary',
44 53 'id' => 'os-toggle',
@@ -45,235 +54,13 @@
45 54 'title' => '<span class="ab-icon dashicons dashicons-desktop" aria-hidden="true"></span>'
46 55 . '<span class="ab-label">' . $label . '</span>',
47 56 'href' => '#',
48 57 'meta' => array(
49 - 'class' => $is_active ? 'os-active' : '',
50 58 'tabindex' => 0,
51 59 'title' => $label,
52 60 ),
53 61 )
54 62 );
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 63 }
277 64 add_action( 'admin_bar_menu', 'openstation_admin_bar_toggle', 190 );
278 65
279 66 /**
@@ -293,19 +80,17 @@
293 80 // Inside a chromeless window the admin bar is suppressed outright
294 81 // (`show_admin_bar` + the `wp_admin_bar_render` removal in
295 82 // helpers.php), so `#wpadminbar` never reaches the DOM. Every byte
296 83 // 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.
84 + // would load and run against markup that does not exist. See
85 + // `includes/render/chromeless-trim.php`, which drops the rest of
86 + // that family (core's `admin-bar`, host masterbar extras) for the
87 + // same reason.
303 88 if ( openstation_is_chromeless_request() ) {
304 89 return;
305 90 }
306 91
307 - // The items are `display: flex`, never `inline-flex`. An
92 + // The item is `display: flex`, never `inline-flex`. An
308 93 // inline-level box sits on a line box the <li> lays out at its
309 94 // 32px line-height and aligns on the baseline, so the line grows
310 95 // by the descender: the item became 37px inside a 32px bar. Under
311 96 // Core's float layout that overflow was invisible. On a host that
@@ -316,22 +101,14 @@
316 101 // is exactly the bar. `Tests_OpenStation_AdminBarDesktopToggle`
317 102 // pins it; `desktop.css` caps the group as well, for items we do
318 103 // not own.
319 104 $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 {
105 + #wpadminbar #wp-admin-bar-os-toggle > .ab-item {
325 106 display: flex;
326 107 align-items: center;
327 108 gap: 6px;
328 109 }
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 {
110 + #wpadminbar #wp-admin-bar-os-toggle .ab-icon {
334 111 float: none;
335 112 margin: 0;
336 113 padding: 0;
337 114 display: inline-flex;
@@ -339,11 +116,9 @@
339 116 justify-content: center;
340 117 width: 20px;
341 118 height: 20px;
342 119 }
343 -
344 - #wp-admin-bar-os-toggle .ab-icon.dashicons,
345 - #wp-admin-bar-desktop-layout-menu .ab-icon.dashicons {
120 + #wp-admin-bar-os-toggle .ab-icon.dashicons {
346 121 font: normal 20px/1 dashicons;
347 122 -webkit-font-smoothing: antialiased;
348 123 -moz-osx-font-smoothing: grayscale;
349 124 }
@@ -351,335 +126,21 @@
351 126 content: "\f472";
352 127 top: 0;
353 128 position: static;
354 129 }
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 130 @media screen and (max-width: 782px) {
369 - #wp-admin-bar-os-toggle .ab-label {
131 + /* WP core hides most admin-bar items on mobile. Force the toggle to
132 + stay visible so users can switch back to OpenStation/Classic. */
133 + #wpadminbar #wp-admin-bar-os-toggle {
134 + display: block;
135 + }
136 + #wpadminbar #wp-admin-bar-os-toggle > .ab-item {
137 + padding: 0 14px;
138 + }
139 + #wpadminbar #wp-admin-bar-os-toggle .ab-label {
370 140 display: none;
371 141 }
372 142 }
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 143 ';
683 144 wp_add_inline_style( 'admin-bar', $css );
684 145
685 146 // All PHP→JS values are emitted as JSON literals (never interpolated
@@ -684,14 +145,13 @@
684 145
685 146 // All PHP→JS values are emitted as JSON literals (never interpolated
686 147 // raw into the script body) so special characters, quotes, and
687 148 // 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.
149 + //
150 + // The config is emitted on every admin screen, including the ones
151 + // where the toggle node is absent: the shell reads the same global
152 + // for the "Exit OpenStation" tile's AJAX call and for the
153 + // keyboard-shortcuts window.
694 154 wp_register_script(
695 155 'os-admin-bar',
696 156 OPENSTATION_URL . 'assets/js/admin-bar.js',
697 157 array( 'admin-bar' ),
@@ -697,85 +157,78 @@
697 157 array( 'admin-bar' ),
698 158 OPENSTATION_VERSION,
699 159 true
700 160 );
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.
161 + // Emit the config as a JSON literal via wp_add_inline_script, not
162 + // wp_localize_script — the latter casts every value to a string,
163 + // so `network` would arrive as '' / '1'. 'before' runs ahead of
164 + // admin-bar.js so the global is ready when the IIFE fires.
705 165 wp_add_inline_script(
706 166 'os-admin-bar',
707 167 'var openStationAdminBar = ' . wp_json_encode(
708 168 array(
709 169 'nonce' => wp_create_nonce( 'save-openstation' ),
710 - 'active' => openstation_is_enabled() && ! openstation_is_classic_request(),
711 170 // `self_admin_url()`: switching off from the network
712 171 // admin returns there, not to the main site.
713 172 'classicUrl' => esc_url_raw( self_admin_url() ),
714 173 '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.
174 + // Passed back on the enable / disable AJAX call: the
175 + // handler runs on `admin-ajax.php`, where
176 + // `is_network_admin()` is always false.
718 177 'network' => is_network_admin(),
719 178 '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 179
727 180 /*
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.
181 + * Keyboard-shortcuts reference content. Translated once
182 + * on the server and read by `src/shortcuts.ts`, which
183 + * renders the Keyboard shortcuts window.
731 184 *
732 185 * `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.
186 + * three modes the shortcuts operate in (Outside
187 + * Workspaces, Inside Workspaces, Show Desktop).
188 + * `general` is a flat list for shortcuts whose
189 + * behaviour doesn't shift by mode.
737 190 */
738 191 'shortcuts' => array(
739 192 'title' => __( 'Keyboard shortcuts', 'desktop-mode' ),
740 193 'contextual' => array(
741 - 'heading' => __( 'Desktops & Overview', 'desktop-mode' ),
194 + 'heading' => __( 'Workspaces', 'desktop-mode' ),
742 195 'headers' => array(
743 196 'key' => __( 'Key', 'desktop-mode' ),
744 - 'outside' => __( 'Outside overview', 'desktop-mode' ),
745 - 'inside' => __( 'Inside overview', 'desktop-mode' ),
197 + 'outside' => __( 'Outside Workspaces', 'desktop-mode' ),
198 + 'inside' => __( 'Inside Workspaces', 'desktop-mode' ),
746 199 'showDesktop' => __( 'In Show Desktop', 'desktop-mode' ),
747 200 ),
748 201 'rows' => array(
749 202 array(
750 203 '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' ),
204 + 'outside' => __( 'Previous workspace (wraps)', 'desktop-mode' ),
205 + 'inside' => __( 'Previous workspace (grid + top-bar update)', 'desktop-mode' ),
206 + 'showDesktop' => __( 'Previous workspace', 'desktop-mode' ),
754 207 ),
755 208 array(
756 209 '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' ),
210 + 'outside' => __( 'Next workspace (wraps)', 'desktop-mode' ),
211 + 'inside' => __( 'Next workspace (grid + top-bar update)', 'desktop-mode' ),
212 + 'showDesktop' => __( 'Next workspace', 'desktop-mode' ),
760 213 ),
761 214 array(
762 215 'keys' => array( '↑' ),
763 - 'outside' => __( 'Enter Overview', 'desktop-mode' ),
764 - 'inside' => __( 'Exit Overview onto active desktop', 'desktop-mode' ),
216 + 'outside' => __( 'Enter Workspaces', 'desktop-mode' ),
217 + 'inside' => __( 'Exit onto the active workspace', 'desktop-mode' ),
765 218 'showDesktop' => __( 'Restore windows (exit Show Desktop)', 'desktop-mode' ),
766 219 ),
767 220 array(
768 221 'keys' => array( '↓' ),
769 222 'outside' => __( 'Toggle Show Desktop', 'desktop-mode' ),
770 - 'inside' => __( 'Exit Overview (no minimize)', 'desktop-mode' ),
223 + 'inside' => __( 'Exit Workspaces (no minimize)', 'desktop-mode' ),
771 224 'showDesktop' => __( 'Toggle Show Desktop', 'desktop-mode' ),
772 225 ),
773 226 array(
774 227 'keys' => array( 'Enter' ),
775 - 'note' => __( '(in overview)', 'desktop-mode' ),
228 + 'note' => __( '(in Workspaces)', 'desktop-mode' ),
776 229 'outside' => '—',
777 - 'inside' => __( 'Commit current desktop, exit overview', 'desktop-mode' ),
230 + 'inside' => __( 'Commit current workspace, exit Workspaces', 'desktop-mode' ),
778 231 'showDesktop' => '—',
779 232 ),
780 233 ),
781 234 ),
@@ -783,13 +236,13 @@
783 236 'heading' => __( 'Windows & palette', 'desktop-mode' ),
784 237 'items' => array(
785 238 array(
786 239 'keys' => array( '`' ),
787 - 'description' => __( 'Cycle to the next window on the active desktop.', 'desktop-mode' ),
240 + 'description' => __( 'Cycle to the next window on the active workspace.', 'desktop-mode' ),
788 241 ),
789 242 array(
790 243 'keys' => array( 'Shift', '`' ),
791 - 'description' => __( 'Cycle to the previous window on the active desktop.', 'desktop-mode' ),
244 + 'description' => __( 'Cycle to the previous window on the active workspace.', 'desktop-mode' ),
792 245 ),
793 246 array(
794 247 'keys' => array( '⌘/Ctrl', 'K' ),
795 248 'description' => __( 'Open the command palette / Ask AI overlay.', 'desktop-mode' ),
@@ -795,13 +248,13 @@
795 248 'description' => __( 'Open the command palette / Ask AI overlay.', 'desktop-mode' ),
796 249 ),
797 250 array(
798 251 'keys' => array( '⌥/Alt', '⌘/Ctrl', 'W' ),
799 - 'description' => __( 'Close every open window on the current desktop (asks first).', 'desktop-mode' ),
252 + 'description' => __( 'Close every open window on the current workspace (asks first).', 'desktop-mode' ),
800 253 ),
801 254 array(
802 255 'keys' => array( 'Esc' ),
803 - 'description' => __( 'Exit Overview (or Snap Overview) without changing window state.', 'desktop-mode' ),
256 + 'description' => __( 'Exit Workspaces (or Snap Overview) without changing window state.', 'desktop-mode' ),
804 257 ),
805 258 ),
806 259 ),
807 260 ),