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