add_node(
array(
'parent' => 'top-secondary',
'id' => 'os-toggle',
'title' => ''
. '' . $label . '',
'href' => '#',
'meta' => array(
'class' => $is_active ? 'os-active' : '',
'tabindex' => 0,
'title' => $label,
),
)
);
// Fullscreen toggle — sits next to "Switch to Classic Admin" so the
// shell can occupy the whole screen without browser chrome. Only
// makes sense in OpenStation; kept out of classic where it would
// just be a redundant browser-fullscreen shortcut.
if ( $is_active ) {
$wp_admin_bar->add_node(
array(
'parent' => 'top-secondary',
'id' => 'desktop-fullscreen',
'title' => '',
'href' => '#',
'meta' => array(
'class' => 'desktop-fullscreen-btn',
'title' => __( 'Enter fullscreen', 'desktop-mode' ),
'tabindex' => 0,
),
)
);
}
// Layout menu — only surfaced when the user is actually viewing
// the desktop shell (the actions don't make sense in classic
// admin, which has no windows to arrange). Parent renders as a
// dashicon with a hover-opened submenu; each child is routed to
// `wp.os.windowManager.*` by the inline JS.
//
// Sits next to Fullscreen so the two shell-state actions group
// visually. Ask AI, then the help/meta cluster (Keyboard shortcuts
// + Report a bug) follow, with the help/meta pair anchored to the
// far right of the secondary bar next to the user identity menu.
if ( $is_active ) {
$wp_admin_bar->add_node(
array(
'parent' => 'top-secondary',
'id' => 'desktop-layout-menu',
'title' => '',
'href' => '#',
'meta' => array(
'title' => __( 'Arrange windows', 'desktop-mode' ),
'tabindex' => 0,
),
)
);
$wp_admin_bar->add_node(
array(
'parent' => 'desktop-layout-menu',
'id' => 'desktop-layout-cascade',
'title' => esc_html__( 'Cascade', 'desktop-mode' ),
'href' => '#',
'meta' => array(
'class' => 'os-layout-action',
'title' => __( 'Lay all windows out from top-left, offset so every title bar stays visible.', 'desktop-mode' ),
),
)
);
$wp_admin_bar->add_node(
array(
'parent' => 'desktop-layout-menu',
'id' => 'desktop-layout-overview',
'title' => esc_html__( 'Overview', 'desktop-mode' ),
'href' => '#',
'meta' => array(
'class' => 'os-layout-action',
'title' => __( 'Zoom out to see every window at once. Click one to focus it.', 'desktop-mode' ),
),
)
);
// Snap-to-grid toggle. Renders as a checkbox-style entry that
// must NOT dismiss the parent menu on click — see the inline
// JS below for the stop-propagation handling. Initial check
// state is painted from the persisted preference once the
// shell has booted.
$wp_admin_bar->add_node(
array(
'parent' => 'desktop-layout-menu',
'id' => 'desktop-layout-snap',
'title' => '☐ '
. esc_html__( 'Snap to grid', 'desktop-mode' ),
'href' => '#',
'meta' => array(
'class' => 'os-layout-snap',
'title' => __( 'Snap windows to a grid while dragging or resizing.', 'desktop-mode' ),
),
)
);
$wp_admin_bar->add_node(
array(
'parent' => 'desktop-layout-menu',
'id' => 'desktop-layout-tile',
'title' => esc_html__( 'Tile all windows', 'desktop-mode' ),
'href' => '#',
'meta' => array(
'class' => 'os-layout-action',
'title' => __( 'Pack every window into an evenly tiled grid that fills the desktop.', 'desktop-mode' ),
),
)
);
/**
* Filter the list of custom arrange-menu items contributed by
* plugins. Each entry becomes an additional node under the
* "Arrange" submenu, rendered with the same styling as the
* built-in Cascade / Overview / Tile items. Clicking a custom
* item dispatches the JS action `os.arrange.custom-action`
* with payload `{ id }` — plugins subscribe via
* `wp.hooks.addAction()` and run their own arrangement logic.
*
* Each item is an associative array:
*
* 'id' string Unique slug (letters, digits, dashes).
* 'title' string Menu label (already translated).
* 'description' string Optional tooltip / aria description.
* 'position' int Optional sort key; lower sorts earlier.
* Built-ins are effectively at 0-3; use
* 10+ to append.
*
* Entries with missing/invalid `id` or `title` are dropped.
*
* @param array $items Existing custom items (default empty).
*/
$custom = apply_filters( 'openstation_arrange_menu_items', array() );
if ( is_array( $custom ) ) {
// Stable sort by `position` (default 10 — after built-ins),
// preserving registration order within a tie.
$sortable = array();
foreach ( $custom as $index => $item ) {
if ( ! is_array( $item ) ) {
continue;
}
$id = isset( $item['id'] ) ? sanitize_key( (string) $item['id'] ) : '';
$title = isset( $item['title'] ) ? (string) $item['title'] : '';
if ( '' === $id || '' === $title ) {
continue;
}
$sortable[] = array(
'id' => $id,
'title' => $title,
'description' => isset( $item['description'] ) ? (string) $item['description'] : '',
'position' => isset( $item['position'] ) && is_numeric( $item['position'] )
? (int) $item['position']
: 10,
'index' => $index,
);
}
usort(
$sortable,
static function ( $a, $b ) {
if ( $a['position'] === $b['position'] ) {
return $a['index'] - $b['index'];
}
return $a['position'] - $b['position'];
}
);
foreach ( $sortable as $item ) {
// The custom id is round-tripped through the DOM id
// (stripping the `desktop-layout-custom-` prefix in the
// click handler). Keeps us inside the documented
// WP_Admin_Bar::add_node meta surface — no non-standard
// attributes, no custom render callbacks.
$wp_admin_bar->add_node(
array(
'parent' => 'desktop-layout-menu',
'id' => 'desktop-layout-custom-' . $item['id'],
'title' => esc_html( $item['title'] ),
'href' => '#',
'meta' => array(
'class' => 'os-layout-action os-layout-custom',
'title' => $item['description'],
),
)
);
}
}
}
// "Keyboard shortcuts" trigger — shown only when OpenStation is
// active. Clicking toggles the keyboard-shortcuts popover wired by
// assets/js/admin-bar.js (wireShortcutsPopover); the popover content
// is translated server-side and shipped via the `shortcuts` key of
// the openStationAdminBar config blob below.
if ( $is_active ) {
$wp_admin_bar->add_node(
array(
'parent' => 'top-secondary',
'id' => 'desktop-help',
'title' => '',
'href' => '#',
'meta' => array(
'class' => 'desktop-help-btn',
'title' => __( 'Keyboard shortcuts', 'desktop-mode' ),
'tabindex' => 0,
),
)
);
}
// "Report a bug" trigger — shown only when OpenStation is active.
// Clicking dispatches a `os-open-bug-report` document
// CustomEvent that the shell listens for and answers by opening the
// Bug Report native window. PHP doesn't know the JS side exists; the
// event lets us add the button without coupling to any specific
// shell module. Anchored to the far right of the secondary bar so
// the meta/help cluster (Keyboard shortcuts + Report a bug) sits
// next to the user identity menu — matches the convention used by
// most SaaS products (Help / "?" at the user-menu corner).
if ( $is_active ) {
$wp_admin_bar->add_node(
array(
'parent' => 'top-secondary',
'id' => 'desktop-bug-report',
'title' => '',
'href' => '#',
'meta' => array(
'class' => 'desktop-bug-report-btn',
'title' => __( 'Open the Bug Report window', 'desktop-mode' ),
'tabindex' => 0,
),
)
);
}
}
add_action( 'admin_bar_menu', 'openstation_admin_bar_toggle', 190 );
/**
* Enqueues the CSS and JS for the OpenStation toggle.
*
* The CSS is inline, attached to the `admin-bar` style handle so it always
* ships with the admin bar itself — no matter which admin screen is showing.
* The JS is the external assets/js/admin-bar.js bundle, registered as
* `os-admin-bar` with `admin-bar` as a dependency; its config is
* emitted as an inline JSON literal `before` the script.
*/
function openstation_enqueue_toggle_assets() {
if ( ! is_admin() || ! is_user_logged_in() ) {
return;
}
// Inside a chromeless window the admin bar is suppressed outright
// (`show_admin_bar` + the `wp_admin_bar_render` removal in
// helpers.php), so `#wpadminbar` never reaches the DOM. Every byte
// below — the toggle bundle, its inline config, the node styling —
// would load and run against markup that does not exist. Measured
// on a live install: the toggle bundle alone was 17.7 KB, the
// largest single asset in the admin-bar family a window loaded for
// nothing. See `includes/render/chromeless-trim.php`, which drops
// the rest of that family (core's `admin-bar`, host masterbar
// extras) for the same reason.
if ( openstation_is_chromeless_request() ) {
return;
}
// The items are `display: flex`, never `inline-flex`. An
// inline-level box sits on a line box the