| @@ -1,11 +1,11 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | - * Desktop Mode — Asset enqueue. | |
| 3 | + * OpenStation — Asset enqueue. | |
| 4 | 4 | * |
| 5 | - * Loads the desktop shell CSS + JS bundles when desktop mode is | |
| 5 | + * Loads the desktop shell CSS + JS bundles when OpenStation is | |
| 6 | 6 | * active and the request isn't chromeless / classic-overridden. |
| 7 | - * Owns the entire `desktop_mode_enqueue_assets()` body — the | |
| 7 | + * Owns the entire `openstation_enqueue_assets()` body — the | |
| 8 | 8 | * largest hook in the original render.php and the natural seam |
| 9 | 9 | * for "what does the shell ship to the browser today?". |
| 10 | 10 | * |
| 11 | 11 | * Extracted from `render.php` during the architecture-0.8.1 PHP |
| @@ -10,42 +10,39 @@ | ||
| 10 | 10 | * |
| 11 | 11 | * Extracted from `render.php` during the architecture-0.8.1 PHP |
| 12 | 12 | * slicing (phase 6). |
| 13 | 13 | * |
| 14 | - * @package Desktop_Mode | |
| 15 | - * @since 0.8.1 | |
| 14 | + * @package OpenStation | |
| 16 | 15 | */ |
| 17 | 16 | |
| 18 | 17 | defined( 'ABSPATH' ) || exit; |
| 19 | 18 | |
| 20 | 19 | /** |
| 21 | - * Enqueues the desktop mode shell assets (CSS + JS) when desktop mode is active. | |
| 20 | + * Enqueues the OpenStation shell assets (CSS + JS) when OpenStation is active. | |
| 22 | 21 | * |
| 23 | 22 | * Only loads the full desktop shell scripts and styles when the user has |
| 24 | - * desktop mode enabled and the request is not a chromeless iframe load. | |
| 25 | - * | |
| 26 | - * @since 0.1.0 | |
| 23 | + * OpenStation enabled and the request is not a chromeless iframe load. | |
| 27 | 24 | */ |
| 28 | -function desktop_mode_enqueue_assets() { | |
| 25 | +function openstation_enqueue_assets() { | |
| 29 | 26 | if ( ! is_admin() ) { |
| 30 | 27 | return; |
| 31 | 28 | } |
| 32 | 29 | |
| 33 | - // Auto-enqueue the iframe bridge anywhere a desktop-mode user | |
| 30 | + // Auto-enqueue the iframe bridge anywhere a openstation user | |
| 34 | 31 | // might land. The bundle self-bails when not inside an iframe |
| 35 | 32 | // (`window.parent === window`), so it's a no-op on the parent |
| 36 | 33 | // shell — but cheap insurance against the failure mode the |
| 37 | 34 | // developer hit: an internal admin navigation drops the |
| 38 | - // `?desktop_mode_chromeless=1` flag, the chromeless inline bridge doesn't | |
| 39 | - // run, and `wp.desktop.iframe` silently disappears. With this | |
| 35 | + // `?openstation_chromeless=1` flag, the chromeless inline bridge doesn't | |
| 36 | + // run, and `wp.os.iframe` silently disappears. With this | |
| 40 | 37 | // auto-enqueue, the API is universally present for any same- |
| 41 | - // origin admin page a desktop-mode user opens — chromeless or | |
| 38 | + // origin admin page a openstation user opens — chromeless or | |
| 42 | 39 | // accidentally classic. |
| 43 | - if ( desktop_mode_is_enabled() ) { | |
| 44 | - wp_enqueue_script( 'desktop-mode-iframe-bridge' ); | |
| 40 | + if ( openstation_is_enabled() ) { | |
| 41 | + wp_enqueue_script( 'os-iframe-bridge' ); | |
| 45 | 42 | |
| 46 | 43 | // Block Editor cross-window drop receiver. Listens for |
| 47 | - // `desktop-mode-drop` postMessages from the parent shell and | |
| 44 | + // `os-drop` postMessages from the parent shell and | |
| 48 | 45 | // inserts the matching block. Only enqueue inside the |
| 49 | 46 | // post-edit Block Editor screens — every other admin page |
| 50 | 47 | // would be paying for a bundle it never uses. |
| 51 | 48 | // |
| @@ -56,48 +53,107 @@ | ||
| 56 | 53 | // after the receiver's 5 s `waitForEditor()` poll. Re-enable |
| 57 | 54 | // once we have a reliable readiness signal in that context. |
| 58 | 55 | global $hook_suffix; |
| 59 | 56 | if ( 'post.php' === $hook_suffix || 'post-new.php' === $hook_suffix ) { |
| 60 | - wp_enqueue_script( 'desktop-mode-gutenberg-drop-receiver' ); | |
| 57 | + wp_enqueue_script( 'os-gutenberg-drop-receiver' ); | |
| 61 | 58 | } |
| 62 | 59 | } |
| 63 | 60 | |
| 64 | 61 | // Chromeless requests (iframes) need chromeless styles and overrides. |
| 65 | - if ( desktop_mode_is_chromeless_request() ) { | |
| 66 | - wp_enqueue_style( 'desktop-mode' ); | |
| 67 | - wp_enqueue_style( 'desktop-mode-chromeless' ); | |
| 62 | + if ( openstation_is_chromeless_request() ) { | |
| 63 | + wp_enqueue_style( 'openstation' ); | |
| 64 | + wp_enqueue_style( 'os-chromeless' ); | |
| 68 | 65 | |
| 69 | 66 | /** |
| 70 | - * Fires when chromeless styles are enqueued inside a desktop mode iframe. | |
| 67 | + * Fires when chromeless styles are enqueued inside a OpenStation iframe. | |
| 71 | 68 | * |
| 72 | 69 | * Plugin and theme authors can hook here to enqueue their own CSS |
| 73 | 70 | * overrides for legacy pages rendered in chromeless mode. Use the |
| 74 | - * `.desktop-mode-chromeless` body class to scope your rules. | |
| 75 | - * | |
| 76 | - * @since 0.1.0 | |
| 71 | + * `.os-chromeless` body class to scope your rules. | |
| 77 | 72 | */ |
| 78 | - do_action( 'desktop_mode_chromeless_styles' ); | |
| 73 | + do_action( 'openstation_chromeless_styles' ); | |
| 79 | 74 | return; |
| 80 | 75 | } |
| 81 | 76 | |
| 82 | - if ( ! desktop_mode_is_enabled() || desktop_mode_is_classic_request() ) { | |
| 77 | + if ( ! openstation_is_shell_request() ) { | |
| 83 | 78 | return; |
| 84 | 79 | } |
| 85 | 80 | |
| 86 | - // CSS. | |
| 87 | - wp_enqueue_style( 'desktop-mode' ); | |
| 88 | - wp_enqueue_style( 'desktop-mode-windows' ); | |
| 89 | - wp_enqueue_style( 'desktop-mode-window-overview' ); | |
| 90 | - wp_enqueue_style( 'desktop-mode-os-settings' ); | |
| 91 | - wp_enqueue_style( 'desktop-mode-dock' ); | |
| 92 | - wp_enqueue_style( 'desktop-mode-dock-peek' ); | |
| 93 | - wp_enqueue_style( 'desktop-mode-ai-assistant' ); | |
| 94 | - wp_enqueue_style( 'desktop-mode-bug-report' ); | |
| 95 | - wp_enqueue_style( 'desktop-mode-files' ); | |
| 96 | - wp_enqueue_style( 'desktop-mode-notes' ); | |
| 81 | + // CSS. Only the sheets that paint surfaces present at boot — the | |
| 82 | + // shell chrome, the dock, desktop tiles and pinned notes. Sheets | |
| 83 | + // for on-demand surfaces (Preferences panel, AI assistant, bug | |
| 84 | + // report) ship as `deferredStyles` in the config blob below and | |
| 85 | + // inject on first open; a native window's sheet rides its | |
| 86 | + // registration's `styles` companion list the same way. | |
| 87 | + wp_enqueue_style( 'openstation' ); | |
| 88 | + wp_enqueue_style( 'os-windows' ); | |
| 89 | + wp_enqueue_style( 'os-window-overview' ); | |
| 90 | + wp_enqueue_style( 'os-dock' ); | |
| 91 | + wp_enqueue_style( 'os-dock-peek' ); | |
| 92 | + wp_enqueue_style( 'os-notch' ); | |
| 93 | + wp_enqueue_style( 'os-workspaces' ); | |
| 94 | + wp_enqueue_style( 'os-shortcuts' ); | |
| 95 | + wp_enqueue_style( 'os-openstation-layout' ); | |
| 96 | + wp_enqueue_style( 'os-files' ); | |
| 97 | + wp_enqueue_style( 'os-notes' ); | |
| 98 | + // Unconditional like the layout sheet: a live crossing into the | |
| 99 | + // phone band must not find the phone layer unstyled. | |
| 100 | + wp_enqueue_style( 'os-mobile' ); | |
| 97 | 101 | |
| 102 | + // Solo mode — a single window freed into a native OS window by the | |
| 103 | + // desktop host. Same shell, everything but that one window hidden. | |
| 104 | + $solo_window = openstation_solo_window_id(); | |
| 105 | + if ( '' !== $solo_window ) { | |
| 106 | + wp_enqueue_style( 'os-solo' ); | |
| 107 | + | |
| 108 | + /* | |
| 109 | + * Hide every window that is not the one this surface was booted | |
| 110 | + * to paint — from the first frame, before any of them exist. | |
| 111 | + * | |
| 112 | + * Solo mode promises one window. Anything that opens a second | |
| 113 | + * (a game launched from a freed Games hub, a plugin calling | |
| 114 | + * `openWindow`) would otherwise land on top of the first, and | |
| 115 | + * solo's CSS stretches every window to fill the viewport, so it | |
| 116 | + * covers what the user was using. | |
| 117 | + * | |
| 118 | + * This has to be CSS rather than JavaScript, and it has to be | |
| 119 | + * inline. A JS rule can only run once the window exists, which | |
| 120 | + * is a frame too late — the user sees the newcomer flash before | |
| 121 | + * it is dealt with. A static stylesheet cannot express it | |
| 122 | + * either, because the selector depends on which window this is. | |
| 123 | + * So the rule is emitted with the id baked in, and no window but | |
| 124 | + * that one is ever painted. | |
| 125 | + * | |
| 126 | + * `visibility` rather than `display`: a hidden-but-laid-out | |
| 127 | + * window still has a size, which canvas-based windows need in | |
| 128 | + * order to initialise without dividing by zero on the way to | |
| 129 | + * being closed. | |
| 130 | + * | |
| 131 | + * The id is `sanitize_key()`-clean (see `openstation_solo_window_id()`), | |
| 132 | + * so it is safe in a selector; it is escaped again here because | |
| 133 | + * the distance between those two facts is exactly where this | |
| 134 | + * kind of bug lives. | |
| 135 | + */ | |
| 136 | + wp_add_inline_style( | |
| 137 | + 'os-solo', | |
| 138 | + sprintf( | |
| 139 | + 'body.os-solo .os-window:not(#wp-window-%1$s){visibility:hidden !important;pointer-events:none !important;}', | |
| 140 | + esc_attr( $solo_window ) | |
| 141 | + ) | |
| 142 | + ); | |
| 143 | + } | |
| 144 | + | |
| 145 | + // The rebrand announcement paints on one visit per user and never | |
| 146 | + // again, so its stylesheet is only worth sending to the users who | |
| 147 | + // are actually going to see it. Computed once here and reused for | |
| 148 | + // the `rebrandNotice` config key below, which reads the same answer. | |
| 149 | + $show_rebrand_notice = openstation_should_show_rebrand_notice(); | |
| 150 | + if ( $show_rebrand_notice ) { | |
| 151 | + wp_enqueue_style( 'os-announce' ); | |
| 152 | + } | |
| 153 | + | |
| 98 | 154 | // JS. |
| 99 | - wp_enqueue_script( 'desktop-mode' ); | |
| 155 | + wp_enqueue_script( 'openstation' ); | |
| 100 | 156 | |
| 101 | 157 | // `wp_enqueue_command_palette_assets()` (WP 6.9+) enqueues the |
| 102 | 158 | // `wp-commands` store package, the `wp-core-commands` script that |
| 103 | 159 | // registers the WordPress-wide baseline (Add new post, Manage |
| @@ -111,43 +167,45 @@ | ||
| 111 | 167 | // WP normally only calls this on screens that opt in to the native |
| 112 | 168 | // palette; the shell needs it on every admin URL it might wrap. |
| 113 | 169 | // `function_exists` guard for pre-6.9 sites — the harvester gracefully |
| 114 | 170 | // no-ops when the store is missing. |
| 171 | + // See `openstation_defer_core_command_palette()` below for why | |
| 172 | + // Core's own boot-time enqueue is unhooked on shell pages. | |
| 173 | + // | |
| 174 | + // The Core command-palette runtime is NOT enqueued here any more. | |
| 175 | + // Its dependency chain is the whole Gutenberg runtime (~800 KB | |
| 176 | + // gzipped across forty-odd bundles), paid on every boot for a ⌘K | |
| 177 | + // palette most sessions never open. It now ships as an ordered | |
| 178 | + // manifest in the config blob (`commandPalette`, built by | |
| 179 | + // `openstation_build_command_palette_assets_payload()`), and | |
| 180 | + // `src/commands/palette-assets.ts` replays it the first time the | |
| 181 | + // palette is invoked. The shell harvester keeps its idle-time | |
| 182 | + // `install()` — a graceful no-op until the store exists — and | |
| 183 | + // re-installs on `os-command-palette-ready`. | |
| 184 | + $command_palette = openstation_build_command_palette_assets_payload(); | |
| 185 | + | |
| 115 | 186 | if ( function_exists( 'wp_enqueue_command_palette_assets' ) ) { |
| 116 | - // `wp_enqueue_command_palette_assets()` calls | |
| 117 | - // `array_key_exists( $menu_slug, $submenu )` without guarding | |
| 118 | - // the global, so an unset `$submenu` (test contexts, edge-case | |
| 119 | - // admin requests where the menu wasn't built yet) blows up | |
| 120 | - // with a TypeError. Initialize defensively before calling. | |
| 121 | - global $menu, $submenu; | |
| 122 | - if ( ! isset( $submenu ) || ! is_array( $submenu ) ) { | |
| 123 | - $submenu = array(); | |
| 124 | - } | |
| 125 | - if ( ! isset( $menu ) || ! is_array( $menu ) ) { | |
| 126 | - $menu = array(); | |
| 127 | - } | |
| 128 | - wp_enqueue_command_palette_assets(); | |
| 129 | - | |
| 130 | 187 | // Expose the same menu-commands array WP serializes into |
| 131 | 188 | // `wp.coreCommands.initializeCommandPalette(...)` on a window |
| 132 | 189 | // slot the shell harvester can read. Built in PHP from `$menu` |
| 133 | - // / `$submenu` here (we already guarded that they're arrays | |
| 134 | - // above), then injected as a `before` inline on our own bundle | |
| 135 | - // — that runs synchronously before `desktop.min.js` boots the | |
| 136 | - // shell harvester, so the lookup is guaranteed populated by | |
| 137 | - // the time `src/commands/shell-harvester.ts` classifies any | |
| 138 | - // command. Decoupled from WP's command-palette mount timing | |
| 139 | - // (which fires from a core-registered hook we can't reorder). | |
| 140 | - $menu_map = desktop_mode_build_command_menu_map(); | |
| 190 | + // / `$submenu`, then injected as a `before` inline on our own | |
| 191 | + // bundle — that runs synchronously before `desktop.min.js` | |
| 192 | + // boots the shell harvester, so the lookup is guaranteed | |
| 193 | + // populated by the time `src/commands/shell-harvester.ts` | |
| 194 | + // classifies any command. Decoupled from WP's command-palette | |
| 195 | + // mount timing (which fires from a core-registered hook we | |
| 196 | + // can't reorder) — and, since the palette bundles went lazy, | |
| 197 | + // from whether they have loaded at all. | |
| 198 | + $menu_map = openstation_build_command_menu_map(); | |
| 141 | 199 | wp_add_inline_script( |
| 142 | - 'desktop-mode', | |
| 143 | - 'window.__desktopModeMenuCommands = ' . wp_json_encode( $menu_map ) . ';', | |
| 200 | + 'openstation', | |
| 201 | + 'window.__openStationMenuCommands = ' . wp_json_encode( $menu_map ) . ';', | |
| 144 | 202 | 'before' |
| 145 | 203 | ); |
| 146 | 204 | } |
| 147 | 205 | |
| 148 | 206 | // Pass configuration to JavaScript. |
| 149 | - global $title, $pagenow, $parent_file, $menu; | |
| 207 | + global $title, $parent_file, $menu; | |
| 150 | 208 | |
| 151 | 209 | $menu_icon = 'dashicons-admin-generic'; |
| 152 | 210 | if ( ! empty( $parent_file ) && ! empty( $menu ) ) { |
| 153 | 211 | foreach ( $menu as $item ) { |
| @@ -159,72 +217,125 @@ | ||
| 159 | 217 | } |
| 160 | 218 | |
| 161 | 219 | // Build dock items from the admin menu. Core pages are ordered |
| 162 | 220 | // first (Dashboard, Posts, Plugins, Users, Settings, …), then |
| 163 | - // plugin-contributed top-level routes. `desktop_mode_dock_placement` | |
| 221 | + // plugin-contributed top-level routes. `openstation_dock_placement` | |
| 164 | 222 | // is the per-item filter escape hatch for hiding. Shared with the |
| 165 | 223 | // REST menu endpoint so live refreshes (post plugin-activation) |
| 166 | 224 | // produce the same ordering as the boot payload. |
| 167 | - $menu_payload = desktop_mode_build_menu_payload(); | |
| 168 | - $dock_items = $menu_payload['dockItems']; | |
| 169 | - $native_windows = isset( $menu_payload['nativeWindows'] ) | |
| 225 | + $menu_payload = openstation_build_menu_payload(); | |
| 226 | + $dock_items = $menu_payload['dockItems']; | |
| 227 | + $native_windows = isset( $menu_payload['nativeWindows'] ) | |
| 170 | 228 | ? $menu_payload['nativeWindows'] |
| 171 | 229 | : array(); |
| 172 | - $server_widgets = isset( $menu_payload['serverWidgets'] ) | |
| 230 | + | |
| 231 | + // The BOOT page prints every registry window's template as a real | |
| 232 | + // `<template>` tag (`openstation_render_native_window_templates()`, | |
| 233 | + // admin_footer @ 20 — before footer scripts, so the tags are in | |
| 234 | + // the DOM before the shell boots and `ensureTemplate()` adopts | |
| 235 | + // them by id). The payload's `templateHtml` copy exists for the | |
| 236 | + // MID-SESSION path — a bridge or probe payload delivering a | |
| 237 | + // window whose plugin activated after the page rendered — so on | |
| 238 | + // the boot config it is ~27 KB of the same markup twice. Strip it | |
| 239 | + // here, and only here: the bridge and probe payloads keep theirs. | |
| 240 | + foreach ( $native_windows as &$native_window_row ) { | |
| 241 | + if ( is_array( $native_window_row ) ) { | |
| 242 | + $native_window_row['templateHtml'] = ''; | |
| 243 | + } | |
| 244 | + } | |
| 245 | + unset( $native_window_row ); | |
| 246 | + $native_window_script_data = isset( $menu_payload['nativeWindowScriptData'] ) | |
| 247 | + ? $menu_payload['nativeWindowScriptData'] | |
| 248 | + : array(); | |
| 249 | + $server_widgets = isset( $menu_payload['serverWidgets'] ) | |
| 173 | 250 | ? $menu_payload['serverWidgets'] |
| 174 | 251 | : array(); |
| 175 | - $server_wallpapers = isset( $menu_payload['serverWallpapers'] ) | |
| 252 | + $server_wallpapers = isset( $menu_payload['serverWallpapers'] ) | |
| 176 | 253 | ? $menu_payload['serverWallpapers'] |
| 177 | 254 | : array(); |
| 178 | - $server_command_scripts = isset( $menu_payload['serverCommandScripts'] ) | |
| 255 | + $server_command_scripts = isset( $menu_payload['serverCommandScripts'] ) | |
| 179 | 256 | ? $menu_payload['serverCommandScripts'] |
| 180 | 257 | : array(); |
| 181 | - $server_commands = isset( $menu_payload['serverCommands'] ) | |
| 258 | + $server_commands = isset( $menu_payload['serverCommands'] ) | |
| 182 | 259 | ? $menu_payload['serverCommands'] |
| 183 | 260 | : array(); |
| 184 | - $server_settings_tab_scripts = isset( $menu_payload['serverSettingsTabScripts'] ) | |
| 261 | + $server_settings_tab_scripts = isset( $menu_payload['serverSettingsTabScripts'] ) | |
| 185 | 262 | ? $menu_payload['serverSettingsTabScripts'] |
| 186 | 263 | : array(); |
| 187 | - $server_settings_tabs = isset( $menu_payload['serverSettingsTabs'] ) | |
| 264 | + $server_settings_tabs = isset( $menu_payload['serverSettingsTabs'] ) | |
| 188 | 265 | ? $menu_payload['serverSettingsTabs'] |
| 189 | 266 | : array(); |
| 190 | 267 | $server_dock_rail_renderer_scripts = isset( $menu_payload['serverDockRailRendererScripts'] ) |
| 191 | 268 | ? $menu_payload['serverDockRailRendererScripts'] |
| 192 | 269 | : array(); |
| 193 | - $server_titlebar_button_scripts = isset( $menu_payload['serverTitleBarButtonScripts'] ) | |
| 270 | + $server_titlebar_button_scripts = isset( $menu_payload['serverTitleBarButtonScripts'] ) | |
| 194 | 271 | ? $menu_payload['serverTitleBarButtonScripts'] |
| 195 | 272 | : array(); |
| 196 | - $server_window_theme_scripts = isset( $menu_payload['serverWindowThemeScripts'] ) | |
| 273 | + $server_window_action_scripts = isset( $menu_payload['serverWindowActionScripts'] ) | |
| 274 | + ? $menu_payload['serverWindowActionScripts'] | |
| 275 | + : array(); | |
| 276 | + $server_window_theme_scripts = isset( $menu_payload['serverWindowThemeScripts'] ) | |
| 197 | 277 | ? $menu_payload['serverWindowThemeScripts'] |
| 198 | 278 | : array(); |
| 199 | - $server_window_themes = isset( $menu_payload['serverWindowThemes'] ) | |
| 279 | + $server_window_themes = isset( $menu_payload['serverWindowThemes'] ) | |
| 200 | 280 | ? $menu_payload['serverWindowThemes'] |
| 201 | 281 | : array(); |
| 202 | - $server_window_control_scripts = isset( $menu_payload['serverWindowControlScripts'] ) | |
| 282 | + $server_window_control_scripts = isset( $menu_payload['serverWindowControlScripts'] ) | |
| 203 | 283 | ? $menu_payload['serverWindowControlScripts'] |
| 204 | 284 | : array(); |
| 205 | - $server_window_controls = isset( $menu_payload['serverWindowControls'] ) | |
| 285 | + $server_window_controls = isset( $menu_payload['serverWindowControls'] ) | |
| 206 | 286 | ? $menu_payload['serverWindowControls'] |
| 207 | 287 | : array(); |
| 208 | - $server_window_slot_scripts = isset( $menu_payload['serverWindowSlotScripts'] ) | |
| 288 | + $server_window_slot_scripts = isset( $menu_payload['serverWindowSlotScripts'] ) | |
| 209 | 289 | ? $menu_payload['serverWindowSlotScripts'] |
| 210 | 290 | : array(); |
| 211 | - $server_window_slots = isset( $menu_payload['serverWindowSlots'] ) | |
| 291 | + $server_window_slots = isset( $menu_payload['serverWindowSlots'] ) | |
| 212 | 292 | ? $menu_payload['serverWindowSlots'] |
| 213 | 293 | : array(); |
| 214 | - $server_window_chrome_scripts = isset( $menu_payload['serverWindowChromeScripts'] ) | |
| 294 | + $server_window_chrome_scripts = isset( $menu_payload['serverWindowChromeScripts'] ) | |
| 215 | 295 | ? $menu_payload['serverWindowChromeScripts'] |
| 216 | 296 | : array(); |
| 217 | - $server_window_chromes = isset( $menu_payload['serverWindowChromes'] ) | |
| 297 | + $server_window_chromes = isset( $menu_payload['serverWindowChromes'] ) | |
| 218 | 298 | ? $menu_payload['serverWindowChromes'] |
| 219 | 299 | : array(); |
| 220 | - $server_window_notices = isset( $menu_payload['serverWindowNotices'] ) | |
| 300 | + $server_window_notices = isset( $menu_payload['serverWindowNotices'] ) | |
| 221 | 301 | ? $menu_payload['serverWindowNotices'] |
| 222 | 302 | : array(); |
| 223 | - $server_games = isset( $menu_payload['serverGames'] ) | |
| 303 | + $server_games = isset( $menu_payload['serverGames'] ) | |
| 224 | 304 | ? $menu_payload['serverGames'] |
| 225 | 305 | : array(); |
| 226 | - $desktop_icons = isset( $menu_payload['desktopIcons'] ) | |
| 306 | + // Boot-time copy of the desktop-theme library. Without it the | |
| 307 | + // shell's registry seeds EMPTY, and the consequences are subtle | |
| 308 | + // rather than obvious: PHP has already applied the user's theme | |
| 309 | + // server-side (stylesheet + shell attribute), but the client | |
| 310 | + // can't resolve the slug to an entry, so it believes nothing is | |
| 311 | + // active. Themed ICONS never paint, and switching back to the | |
| 312 | + // system default no-ops the first time — `applyDesktopTheme()` | |
| 313 | + // dedupes on an `activeId` that was never set. | |
| 314 | + $server_desktop_themes = isset( $menu_payload['serverDesktopThemes'] ) | |
| 315 | + ? $menu_payload['serverDesktopThemes'] | |
| 316 | + : array(); | |
| 317 | + | |
| 318 | + // Slim the theme library for BOOT: `cssText` and `tokens` are | |
| 319 | + // each ~20 KB per theme, and neither is read at boot — the ACTIVE | |
| 320 | + // theme's stylesheet is server-delivered (see | |
| 321 | + // `openstation_enqueue_desktop_theme_style()`, whose stamp | |
| 322 | + // `bootAlreadyApplied()` detects), and an inactive theme's CSS | |
| 323 | + // only matters at the moment the user picks it in the Preferences | |
| 324 | + // picker — which fetches the full entries from | |
| 325 | + // `GET desktop-mode/v1/desktop-themes` (`ensureFullDesktopThemes()` | |
| 326 | + // client-side). `cssDeferred` marks the gap so the shell can tell | |
| 327 | + // a slimmed entry from a theme that genuinely ships no CSS. | |
| 328 | + // Bridge and probe payloads keep full entries. | |
| 329 | + foreach ( $server_desktop_themes as &$desktop_theme_row ) { | |
| 330 | + if ( is_array( $desktop_theme_row ) ) { | |
| 331 | + $desktop_theme_row['cssText'] = ''; | |
| 332 | + $desktop_theme_row['tokens'] = new stdClass(); | |
| 333 | + $desktop_theme_row['cssDeferred'] = true; | |
| 334 | + } | |
| 335 | + } | |
| 336 | + unset( $desktop_theme_row ); | |
| 337 | + $desktop_icons = isset( $menu_payload['desktopIcons'] ) | |
| 227 | 338 | ? $menu_payload['desktopIcons'] |
| 228 | 339 | : array(); |
| 229 | 340 | |
| 230 | 341 | // Files-on-the-Desktop payload (Phase 0+1). Plugin-registered |
| @@ -229,25 +340,25 @@ | ||
| 229 | 340 | |
| 230 | 341 | // Files-on-the-Desktop payload (Phase 0+1). Plugin-registered |
| 231 | 342 | // file types and openers ship as metadata only; the JS side |
| 232 | 343 | // holds the executable handlers and resolves on double-click. |
| 233 | - $server_file_types = function_exists( 'desktop_mode_build_file_types_payload' ) | |
| 234 | - ? desktop_mode_build_file_types_payload() | |
| 344 | + $server_file_types = function_exists( 'openstation_build_file_types_payload' ) | |
| 345 | + ? openstation_build_file_types_payload() | |
| 235 | 346 | : array(); |
| 236 | - $server_file_openers = function_exists( 'desktop_mode_build_file_openers_payload' ) | |
| 237 | - ? desktop_mode_build_file_openers_payload() | |
| 347 | + $server_file_openers = function_exists( 'openstation_build_file_openers_payload' ) | |
| 348 | + ? openstation_build_file_openers_payload() | |
| 238 | 349 | : array(); |
| 239 | - $user_file_associations = function_exists( 'desktop_mode_get_user_file_associations' ) | |
| 240 | - ? desktop_mode_get_user_file_associations( get_current_user_id() ) | |
| 350 | + $user_file_associations = function_exists( 'openstation_get_user_file_associations' ) | |
| 351 | + ? openstation_get_user_file_associations( get_current_user_id() ) | |
| 241 | 352 | : array(); |
| 242 | - $server_wallpaper_menu_items = function_exists( 'desktop_mode_build_wallpaper_menu_items' ) | |
| 243 | - ? desktop_mode_build_wallpaper_menu_items() | |
| 353 | + $server_wallpaper_menu_items = function_exists( 'openstation_build_wallpaper_menu_items' ) | |
| 354 | + ? openstation_build_wallpaper_menu_items() | |
| 244 | 355 | : array(); |
| 245 | 356 | |
| 246 | 357 | /* |
| 247 | 358 | * OS-file drop config — what the browser drop manager will |
| 248 | 359 | * accept when the user drags a file from their native desktop |
| 249 | - * onto any surface inside Desktop Mode (wallpaper, a folder, | |
| 360 | + * onto any surface inside OpenStation (wallpaper, a folder, | |
| 250 | 361 | * a window, or a chromeless iframe). The allowed-mimes list is |
| 251 | 362 | * the user-scoped `get_allowed_mime_types()` (already capability |
| 252 | 363 | * gated by WordPress); the size cap is `wp_max_upload_size()`. |
| 253 | 364 | * |
| @@ -260,14 +371,12 @@ | ||
| 260 | 371 | : array(); |
| 261 | 372 | /** |
| 262 | 373 | * Filter the allowed-mime map used by the OS-file drop manager. |
| 263 | 374 | * |
| 264 | - * @since 0.30.0 | |
| 265 | - * | |
| 266 | 375 | * @param array<string,string> $mimes_map `ext => mime-type` map (same shape `get_allowed_mime_types()` returns). |
| 267 | 376 | * @param int $user_id The current user id. |
| 268 | 377 | */ |
| 269 | - $drop_allowed_mimes_map = apply_filters( 'desktop_mode_drop_allowed_mimes', $drop_allowed_mimes_map, get_current_user_id() ); | |
| 378 | + $drop_allowed_mimes_map = apply_filters( 'openstation_drop_allowed_mimes', $drop_allowed_mimes_map, get_current_user_id() ); | |
| 270 | 379 | $drop_allowed_mimes_map = is_array( $drop_allowed_mimes_map ) ? $drop_allowed_mimes_map : array(); |
| 271 | 380 | $drop_allowed_mimes = array_values( array_unique( array_values( $drop_allowed_mimes_map ) ) ); |
| 272 | 381 | |
| 273 | 382 | $drop_max_size = (int) wp_max_upload_size(); |
| @@ -275,14 +384,12 @@ | ||
| 275 | 384 | * Filter the per-file size cap (in bytes) used by the OS-file |
| 276 | 385 | * drop manager. Returning `0` disables the client-side cap — |
| 277 | 386 | * the server still enforces its own. |
| 278 | 387 | * |
| 279 | - * @since 0.30.0 | |
| 280 | - * | |
| 281 | 388 | * @param int $max_size Default `wp_max_upload_size()`. |
| 282 | 389 | * @param int $user_id The current user id. |
| 283 | 390 | */ |
| 284 | - $drop_max_size = (int) apply_filters( 'desktop_mode_drop_max_size', $drop_max_size, get_current_user_id() ); | |
| 391 | + $drop_max_size = (int) apply_filters( 'openstation_drop_max_size', $drop_max_size, get_current_user_id() ); | |
| 285 | 392 | |
| 286 | 393 | /** |
| 287 | 394 | * Filter the master OS-file drop enable gate. Lets plugins |
| 288 | 395 | * disable the drop manager by role / capability beyond the |
| @@ -288,15 +395,13 @@ | ||
| 288 | 395 | * disable the drop manager by role / capability beyond the |
| 289 | 396 | * default `upload_files` check (e.g. only for admins, or |
| 290 | 397 | * only on specific multisite blogs). |
| 291 | 398 | * |
| 292 | - * @since 0.30.0 | |
| 293 | - * | |
| 294 | 399 | * @param bool $enabled Default — `current_user_can( 'upload_files' )`. |
| 295 | 400 | * @param int $user_id The current user id. |
| 296 | 401 | */ |
| 297 | 402 | $drop_enabled = (bool) apply_filters( |
| 298 | - 'desktop_mode_drop_enabled', | |
| 403 | + 'openstation_drop_enabled', | |
| 299 | 404 | current_user_can( 'upload_files' ), |
| 300 | 405 | get_current_user_id() |
| 301 | 406 | ); |
| 302 | 407 | |
| @@ -307,48 +412,61 @@ | ||
| 307 | 412 | 'maxSize' => $drop_max_size, |
| 308 | 413 | ); |
| 309 | 414 | |
| 310 | 415 | // Lazy-bundle URL builder. Each lazy-loaded bundle (AI Assistant, |
| 311 | - // About-scene, OS Settings panel, shell-overlays, window-system) | |
| 416 | + // OS Settings panel, shell-overlays, window-system) | |
| 312 | 417 | // is `<script>`-injected by the main bundle on demand — they don't |
| 313 | 418 | // go through `wp_register_script`, so they don't pick up WordPress's |
| 314 | 419 | // usual `?ver=<filemtime>` cache-buster. Without one, the browser |
| 315 | 420 | // happily serves a stale cached copy across plugin updates that |
| 316 | - // don't bump `DESKTOP_MODE_VERSION`, and the main bundle's loader | |
| 421 | + // don't bump `OPENSTATION_VERSION`, and the main bundle's loader | |
| 317 | 422 | // fires a `<script>`-loaded event for a file that's missing the |
| 318 | - // fresh `window.desktopMode*` factory the new code expects. | |
| 423 | + // fresh `window.openStation*` factory the new code expects. | |
| 319 | 424 | // |
| 320 | 425 | // Mirror the `$built_version( … )` helper in `includes/assets.php`: |
| 321 | 426 | // prefer the on-disk mtime of the actual file, fall back to the |
| 322 | 427 | // plugin version when the file is missing (dev environments where |
| 323 | 428 | // the bundle hasn't been built yet). |
| 324 | - $suffix = desktop_mode_asset_suffix(); | |
| 429 | + $suffix = openstation_asset_suffix(); | |
| 325 | 430 | $lazy_bundle_url = static function ( $base ) use ( $suffix ) { |
| 326 | - $path = DESKTOP_MODE_DIR . 'assets/js/' . $base . $suffix . '.js'; | |
| 431 | + $path = OPENSTATION_DIR . 'assets/js/' . $base . $suffix . '.js'; | |
| 327 | 432 | $ver = file_exists( $path ) |
| 328 | 433 | ? (string) filemtime( $path ) |
| 329 | - : DESKTOP_MODE_VERSION; | |
| 434 | + : OPENSTATION_VERSION; | |
| 330 | 435 | return esc_url_raw( |
| 331 | - DESKTOP_MODE_URL . 'assets/js/' . $base . $suffix . '.js?ver=' . $ver | |
| 436 | + OPENSTATION_URL . 'assets/js/' . $base . $suffix . '.js?ver=' . $ver | |
| 332 | 437 | ); |
| 333 | 438 | }; |
| 334 | 439 | |
| 335 | - // Build the current page URL from $pagenow + $_GET. Strip the portal | |
| 336 | - // markers so the derived window ID matches what the dock would produce | |
| 337 | - // for the same page — otherwise auto-opening the entry window and | |
| 338 | - // clicking the same dock icon would create a duplicate. | |
| 339 | - $current_query = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 340 | - unset( $current_query[ DESKTOP_MODE_PORTAL_FLAG ], $current_query[ DESKTOP_MODE_PORTAL_INTENT_FLAG ] ); | |
| 341 | - $current_page = admin_url( $pagenow ) . ( ! empty( $current_query ) ? '?' . http_build_query( $current_query ) : '' ); | |
| 440 | + // The page the shell opens first. On the shell screen it is the | |
| 441 | + // validated `target` query arg (else the session's focused window, | |
| 442 | + // the default window, the Dashboard); on a solo boot it is the | |
| 443 | + // request's own URL. Either way the frozen portal flags are gone | |
| 444 | + // from it, so the derived window id matches what the dock would | |
| 445 | + // produce for the same page — otherwise auto-opening the entry | |
| 446 | + // window and clicking the same dock icon would create a duplicate. | |
| 447 | + $boot_target = openstation_shell_boot_target(); | |
| 448 | + $current_page = $boot_target['url']; | |
| 449 | + $from_portal = $boot_target['fromPortal']; | |
| 450 | + $from_portal_intent = $boot_target['fromPortalIntent']; | |
| 342 | 451 | |
| 343 | - $from_portal = ! empty( $_GET[ DESKTOP_MODE_PORTAL_FLAG ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 344 | - $from_portal_intent = ! empty( $_GET[ DESKTOP_MODE_PORTAL_INTENT_FLAG ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 452 | + // On the shell screen `$title` and `$parent_file` describe the | |
| 453 | + // screen ("OpenStation", no menu), not the page about to open. The | |
| 454 | + // dock entry for that page is the identity the entry window folds | |
| 455 | + // into, so its title and icon are the right first paint; the iframe | |
| 456 | + // reports its own title once it lands either way. | |
| 457 | + $current_title = wp_strip_all_tags( (string) $title ); | |
| 458 | + if ( openstation_is_shell_screen_request() ) { | |
| 459 | + $boot_meta = openstation_shell_boot_target_meta( $current_page, $dock_items ); | |
| 460 | + $current_title = wp_strip_all_tags( $boot_meta['title'] ); | |
| 461 | + if ( '' !== $boot_meta['icon'] ) { | |
| 462 | + $menu_icon = $boot_meta['icon']; | |
| 463 | + } | |
| 464 | + } | |
| 345 | 465 | |
| 346 | 466 | /** |
| 347 | 467 | * Filters the desktop shell configuration passed to JavaScript. |
| 348 | 468 | * |
| 349 | - * @since 0.1.0 | |
| 350 | - * | |
| 351 | 469 | * @param array $config { |
| 352 | 470 | * Desktop shell configuration. |
| 353 | 471 | * |
| 354 | 472 | * @type string $currentPage The current admin page URL. |
| @@ -355,20 +473,20 @@ | ||
| 355 | 473 | * @type string $currentTitle The current page title. |
| 356 | 474 | * @type string $currentIcon Dashicon class for the current page. |
| 357 | 475 | * @type string $adminUrl The base admin URL. |
| 358 | 476 | * @type string $colorScheme The active admin color scheme. |
| 359 | - * @type array $dockItems Dock items derived from the admin menu. Core WordPress pages (Dashboard, Posts, Plugins, Users, Settings, CPTs…) are ordered first; plugin-contributed top-level routes (admin.php?page=*) follow. Items hidden via `desktop_mode_dock_placement` are omitted. | |
| 360 | - * @type array $nativeWindows Server-declared native windows (via `desktop_mode_register_window`). Shell registers + syncs tiles based on this list — activation/deactivation is a diff without shell reload. | |
| 361 | - * @type array $serverWidgets Server-declared right-column widgets (via `desktop_mode_register_widget`). Shell syncs the widget registry + dynamically loads plugin scripts so widgets appear in the picker without a shell reload. | |
| 362 | - * @type array $serverWallpapers Server-declared wallpapers (via `desktop_mode_register_wallpaper`). Same lifecycle — shell loads the plugin's JS, reads the full `WallpaperDef` from `window.desktopModeWallpapers[id]`, and registers / unregisters as plugins activate / deactivate. | |
| 363 | - * @type array $serverCommandScripts Script handles opted-in via `desktop_mode_register_command_script`. Shell injects each URL on activation so commands registered by `wp.desktop.registerCommand` appear in the palette live. Deactivation unregisters any commands whose `owner` matches the departing handle. | |
| 364 | - * @type array $serverCommands Server-declared command metadata (via `desktop_mode_register_command`). Advisory today — reserved for future pre-registration shims. | |
| 365 | - * @type array $serverSettingsTabScripts Script handles opted-in via `desktop_mode_register_settings_tab_script`. Shell injects each URL on activation so tabs registered by `wp.desktop.registerSettingsTab` appear in the OS Settings window live. Deactivation unregisters tabs attributable to the departing handle. | |
| 366 | - * @type array $serverSettingsTabs Server-declared settings-tab metadata (via `desktop_mode_register_settings_tab`). Enables live unregistration on plugin deactivation without requiring JS to set `owner`. | |
| 367 | - * @type array $desktopIcons Server-declared desktop icons (via `desktop_mode_register_icon`). Rendered on the wallpaper as clickable shortcut tiles. | |
| 368 | - * @type array $accentColors Swatch list for the OS Settings accent picker. Filterable via `desktop_mode_accent_colors`. | |
| 369 | - * @type array $toastTypes Toast-notification type map. Filterable via `desktop_mode_toast_types`. | |
| 370 | - * @type string $defaultWallpaper Wallpaper slug applied on first boot. Filterable via `desktop_mode_default_wallpaper`. | |
| 477 | + * @type array $dockItems Dock items derived from the admin menu. Core WordPress pages (Dashboard, Posts, Plugins, Users, Settings, CPTs…) are ordered first; plugin-contributed top-level routes (admin.php?page=*) follow. Items hidden via `openstation_dock_placement` are omitted. | |
| 478 | + * @type array $nativeWindows Server-declared native windows (via `openstation_register_window`). Shell registers + syncs tiles based on this list — activation/deactivation is a diff without shell reload. | |
| 479 | + * @type array $serverWidgets Server-declared right-column widgets (via `openstation_register_widget`). Shell syncs the widget registry + dynamically loads plugin scripts so widgets appear in the picker without a shell reload. | |
| 480 | + * @type array $serverWallpapers Server-declared wallpapers (via `openstation_register_wallpaper`). Same lifecycle — shell loads the plugin's JS, reads the full `WallpaperDef` from `window.openStationWallpapers[id]`, and registers / unregisters as plugins activate / deactivate. | |
| 481 | + * @type array $serverCommandScripts Script handles opted-in via `openstation_register_command_script`. Shell injects each URL on activation so commands registered by `wp.os.registerCommand` appear in the palette live. Deactivation unregisters any commands whose `owner` matches the departing handle. | |
| 482 | + * @type array $serverCommands Server-declared command metadata (via `openstation_register_command`). Advisory today — reserved for future pre-registration shims. | |
| 483 | + * @type array $serverSettingsTabScripts Script handles opted-in via `openstation_register_settings_tab_script`. Shell injects each URL on activation so tabs registered by `wp.os.registerSettingsTab` appear in the OS Settings window live. Deactivation unregisters tabs attributable to the departing handle. | |
| 484 | + * @type array $serverSettingsTabs Server-declared settings-tab metadata (via `openstation_register_settings_tab`). Enables live unregistration on plugin deactivation without requiring JS to set `owner`. | |
| 485 | + * @type array $desktopIcons Server-declared desktop icons (via `openstation_register_icon`). Rendered on the wallpaper as clickable shortcut tiles. | |
| 486 | + * @type array $accentColors Swatch list for the OS Settings accent picker. Filterable via `openstation_accent_colors`. | |
| 487 | + * @type array $toastTypes Toast-notification type map. Filterable via `openstation_toast_types`. | |
| 488 | + * @type string $defaultWallpaper Wallpaper slug applied on first boot. Filterable via `openstation_default_wallpaper`. | |
| 371 | 489 | * @type array $session Saved session (windows, focused, updated). |
| 372 | 490 | * @type string $sessionUrl REST endpoint for saving the session. |
| 373 | 491 | * @type string $mediaUrl REST endpoint for media uploads (wp/v2/media). |
| 374 | 492 | * @type string $restUrl REST API root from rest_url(), safe for pretty and plain permalink installs. |
| @@ -376,186 +494,286 @@ | ||
| 376 | 494 | * @type array $defaultWindow { enabled: bool, url: string } — current default-window preference. |
| 377 | 495 | * @type bool $canUpload Whether the user holds the `upload_files` capability. |
| 378 | 496 | * @type string $pluginUrl Plugin base URL (no trailing slash). Used by the shell to locate vendor assets and by plugins to build asset URLs. |
| 379 | 497 | * @type string $pluginVersion Plugin semver string. Surfaced in the OS Settings → About tab; plugins can read it to gate features by version. |
| 498 | + * @type string $aboutFeedUrl Authenticated admin-AJAX URL that returns the cached OpenStation journal feed for the About tab. | |
| 380 | 499 | * @type string $restNonce Nonce for the session REST endpoint. |
| 381 | - * @type string $portalUrl Canonical `/desktop-mode/` URL. | |
| 500 | + * @type string $soloWindow Window id when the shell was asked to paint exactly one window (`?openstation_solo=<id>`); '' otherwise. No dock, taskbar, wallpaper or desk, and no session restore. | |
| 501 | + * @type string $portalUrl Canonical `/openstation/` URL. | |
| 382 | 502 | * @type bool $fromPortal Whether the shell was reached via the portal. |
| 383 | - * @type bool $fromPortalIntent Whether the portal redirect resolved from an explicit `?target=…` (user navigation intent) rather than the session's focused window or the default-window fallback. Distinguishes a bare `/desktop-mode/` visit from a portal-redirected admin-bar click so the shell can honour the URL the user actually asked for. | |
| 384 | - * @type array $seenIntros Slugs of one-time intro dialogs the user has dismissed (e.g. `['posts']`). Native windows gate their first-open intro on this list. | |
| 503 | + * @type bool $fromPortalIntent Whether the portal redirect resolved from an explicit `?target=…` (user navigation intent) rather than the session's focused window or the default-window fallback. Distinguishes a bare `/openstation/` visit from a portal-redirected admin-bar click so the shell can honour the URL the user actually asked for. | |
| 504 | + * @type array $seenIntros Slugs of one-time announcements the user has dismissed (e.g. `['openstation-rebrand']`). | |
| 385 | 505 | * @type string $seenIntrosUrl REST endpoint for the seen-intros surface — POST `/seen` to mark, DELETE the base to reset. |
| 386 | - * @type array $stickyNotes { available: bool } — whether the Gutenberg Guidelines experiment (the `wp_guideline` CPT + `wp_guideline_type` taxonomy) is registered. The shell skips booting the sticky-notes layer when false, avoiding the REST probes that 404 without the experiment. See `desktop_mode_sticky_notes_is_available()`. | |
| 506 | + * @type bool $rebrandNotice Whether to offer this user the one-off announcement explaining the rename from Desktop Mode to OpenStation. True only when migration 5 flagged this user as a Desktop Mode user from before the rename AND they haven't dismissed the `openstation-rebrand` intro. Only ever present in the shell config, so the announcement never reaches the classic admin. | |
| 387 | 507 | * } |
| 388 | 508 | */ |
| 389 | 509 | $config = apply_filters( |
| 390 | - 'desktop_mode_shell_config', | |
| 510 | + 'openstation_shell_config', | |
| 391 | 511 | array( |
| 392 | - 'currentPage' => esc_url( $current_page ), | |
| 393 | - 'currentTitle' => wp_strip_all_tags( $title ), | |
| 394 | - 'currentIcon' => sanitize_html_class( $menu_icon ), | |
| 395 | - 'adminUrl' => esc_url( admin_url() ), | |
| 396 | - 'colorScheme' => sanitize_html_class( get_user_option( 'admin_color' ), 'fresh' ), | |
| 397 | - 'dockItems' => $dock_items, | |
| 512 | + 'currentPage' => esc_url( $current_page ), | |
| 513 | + 'currentTitle' => $current_title, | |
| 514 | + 'currentIcon' => sanitize_html_class( $menu_icon ), | |
| 515 | + // `self_admin_url()`: the base a window id is derived from, | |
| 516 | + // and the URL the shell leaves for on exit. Both want the | |
| 517 | + // admin the screen is in, which is the network one when the | |
| 518 | + // network shell screen is what rendered. | |
| 519 | + 'adminUrl' => esc_url( self_admin_url() ), | |
| 520 | + 'homeUrl' => esc_url( home_url( '/' ) ), | |
| 521 | + // Decoded: the shell assigns this to `window.location`, | |
| 522 | + // where `&` would make `_wpnonce` arrive as | |
| 523 | + // `amp;_wpnonce` and fail the nonce check. | |
| 524 | + 'logoutUrl' => esc_url_raw( | |
| 525 | + html_entity_decode( wp_logout_url(), ENT_QUOTES, 'UTF-8' ) | |
| 526 | + ), | |
| 527 | + 'colorScheme' => sanitize_html_class( get_user_option( 'admin_color' ), 'fresh' ), | |
| 528 | + 'dockItems' => $dock_items, | |
| 398 | 529 | // Baseline menu fingerprint. The shell seeds its last-known |
| 399 | 530 | // signature from this so the first off-allowlist menu change |
| 400 | 531 | // (vs. this boot state) is caught without a wasted probe. GH#325. |
| 401 | - 'menuSig' => isset( $menu_payload['menuSig'] ) ? (string) $menu_payload['menuSig'] : '', | |
| 402 | - 'nativeWindows' => $native_windows, | |
| 403 | - 'serverWidgets' => $server_widgets, | |
| 404 | - 'serverWallpapers' => $server_wallpapers, | |
| 405 | - 'serverCommandScripts' => $server_command_scripts, | |
| 406 | - 'serverCommands' => $server_commands, | |
| 407 | - 'serverSettingsTabScripts' => $server_settings_tab_scripts, | |
| 408 | - 'serverSettingsTabs' => $server_settings_tabs, | |
| 532 | + 'menuSig' => isset( $menu_payload['menuSig'] ) ? (string) $menu_payload['menuSig'] : '', | |
| 533 | + 'nativeWindows' => $native_windows, | |
| 534 | + // Handle-keyed script data the entries above reference — | |
| 535 | + // one copy per bundle, not one per window. See | |
| 536 | + // `openstation_collect_native_windows_payload()`. | |
| 537 | + 'nativeWindowScriptData' => $native_window_script_data, | |
| 538 | + 'serverWidgets' => $server_widgets, | |
| 539 | + 'serverWallpapers' => $server_wallpapers, | |
| 540 | + 'serverCommandScripts' => $server_command_scripts, | |
| 541 | + 'serverCommands' => $server_commands, | |
| 542 | + 'serverSettingsTabScripts' => $server_settings_tab_scripts, | |
| 543 | + 'serverSettingsTabs' => $server_settings_tabs, | |
| 409 | 544 | 'serverDockRailRendererScripts' => $server_dock_rail_renderer_scripts, |
| 410 | - 'serverTitleBarButtonScripts' => $server_titlebar_button_scripts, | |
| 411 | - 'serverWindowThemeScripts' => $server_window_theme_scripts, | |
| 412 | - 'serverWindowThemes' => $server_window_themes, | |
| 413 | - 'serverWindowControlScripts' => $server_window_control_scripts, | |
| 414 | - 'serverWindowControls' => $server_window_controls, | |
| 415 | - 'serverWindowSlotScripts' => $server_window_slot_scripts, | |
| 416 | - 'serverWindowSlots' => $server_window_slots, | |
| 417 | - 'serverWindowChromeScripts' => $server_window_chrome_scripts, | |
| 418 | - 'serverWindowChromes' => $server_window_chromes, | |
| 419 | - 'serverWindowNotices' => $server_window_notices, | |
| 545 | + 'serverTitleBarButtonScripts' => $server_titlebar_button_scripts, | |
| 546 | + 'serverWindowActionScripts' => $server_window_action_scripts, | |
| 547 | + 'serverWindowThemeScripts' => $server_window_theme_scripts, | |
| 548 | + 'serverWindowThemes' => $server_window_themes, | |
| 549 | + 'serverWindowControlScripts' => $server_window_control_scripts, | |
| 550 | + 'serverWindowControls' => $server_window_controls, | |
| 551 | + 'serverWindowSlotScripts' => $server_window_slot_scripts, | |
| 552 | + 'serverWindowSlots' => $server_window_slots, | |
| 553 | + 'serverWindowChromeScripts' => $server_window_chrome_scripts, | |
| 554 | + 'serverWindowChromes' => $server_window_chromes, | |
| 555 | + 'serverWindowNotices' => $server_window_notices, | |
| 420 | 556 | // Boot-time copy of the payload's `serverGames` — the same |
| 421 | 557 | // list the live-refresh path applies. Without it the games |
| 422 | 558 | // registry only fills after the first chromeless |
| 423 | 559 | // full-payload refresh and the Games hub boots empty. |
| 424 | - 'serverGames' => $server_games, | |
| 425 | - 'desktopIcons' => $desktop_icons, | |
| 426 | - 'serverFileTypes' => $server_file_types, | |
| 427 | - 'serverFileOpeners' => $server_file_openers, | |
| 428 | - 'userFileAssociations' => $user_file_associations, | |
| 429 | - 'filesUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/files' ) ), | |
| 560 | + 'serverGames' => $server_games, | |
| 561 | + 'serverDesktopThemes' => $server_desktop_themes, | |
| 562 | + 'desktopIcons' => $desktop_icons, | |
| 563 | + 'serverFileTypes' => $server_file_types, | |
| 564 | + 'serverFileOpeners' => $server_file_openers, | |
| 565 | + 'userFileAssociations' => $user_file_associations, | |
| 566 | + 'filesUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/files' ) ), | |
| 430 | 567 | // Pinned-notes REST base (`includes/notes/rest.php`). The |
| 431 | 568 | // notes layer boots only when this is present. |
| 432 | - 'notesUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/notes' ) ), | |
| 569 | + 'notesUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/notes' ) ), | |
| 433 | 570 | // Gates the "Convert to post" note affordance — the convert |
| 434 | 571 | // route (and its dock drop target) only make sense for users |
| 435 | 572 | // who can author posts. |
| 436 | - 'canCreatePosts' => current_user_can( 'edit_posts' ), | |
| 437 | - 'serverWallpaperMenuItems' => $server_wallpaper_menu_items, | |
| 438 | - 'accentColors' => desktop_mode_get_accent_colors(), | |
| 439 | - 'toastTypes' => desktop_mode_get_toast_types(), | |
| 440 | - 'coreUpdate' => desktop_mode_get_core_update(), | |
| 441 | - 'coreNotices' => desktop_mode_get_core_notices(), | |
| 442 | - 'pluginNotices' => desktop_mode_get_plugin_notices(), | |
| 443 | - 'defaultWallpaper' => desktop_mode_get_default_wallpaper(), | |
| 444 | - 'session' => desktop_mode_get_session( get_current_user_id() ), | |
| 445 | - 'sessionUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/session' ) ), | |
| 446 | - 'restUrl' => esc_url_raw( rest_url() ), | |
| 447 | - 'mediaUrl' => esc_url_raw( rest_url( 'wp/v2/media' ) ), | |
| 448 | - 'dropConfig' => $drop_config, | |
| 449 | - 'defaultWindowUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/default-window' ) ), | |
| 450 | - 'defaultWindow' => desktop_mode_get_default_window( get_current_user_id() ), | |
| 451 | - 'canUpload' => current_user_can( 'upload_files' ), | |
| 452 | - 'pluginUrl' => esc_url_raw( untrailingslashit( DESKTOP_MODE_URL ) ), | |
| 453 | - 'pluginVersion' => DESKTOP_MODE_VERSION, | |
| 454 | - 'iframeBridgeUrl' => $lazy_bundle_url( 'iframe-bridge' ), | |
| 573 | + 'canCreatePosts' => current_user_can( 'edit_posts' ), | |
| 574 | + 'serverWallpaperMenuItems' => $server_wallpaper_menu_items, | |
| 575 | + 'accentColors' => openstation_get_accent_colors(), | |
| 576 | + 'toastTypes' => openstation_get_toast_types(), | |
| 577 | + 'coreUpdate' => openstation_get_core_update(), | |
| 578 | + 'coreNotices' => openstation_get_core_notices(), | |
| 579 | + 'pluginNotices' => openstation_get_plugin_notices(), | |
| 580 | + 'defaultWallpaper' => openstation_get_default_wallpaper(), | |
| 581 | + 'session' => openstation_get_session( get_current_user_id() ), | |
| 582 | + // The session route runs in the main site's blog context | |
| 583 | + // whichever desktop posts to it, so the network screen's | |
| 584 | + // URL says which session it is addressing — see | |
| 585 | + // `openstation_rest_session_network()`. | |
| 586 | + 'sessionUrl' => esc_url_raw( | |
| 587 | + is_network_admin() | |
| 588 | + ? add_query_arg( 'network', '1', rest_url( 'desktop-mode/v1/session' ) ) | |
| 589 | + : rest_url( 'desktop-mode/v1/session' ) | |
| 590 | + ), | |
| 591 | + 'restUrl' => esc_url_raw( rest_url() ), | |
| 592 | + 'mediaUrl' => esc_url_raw( rest_url( 'wp/v2/media' ) ), | |
| 593 | + 'dropConfig' => $drop_config, | |
| 594 | + 'defaultWindowUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/default-window' ) ), | |
| 595 | + 'defaultWindow' => openstation_get_default_window( get_current_user_id() ), | |
| 596 | + 'canUpload' => current_user_can( 'upload_files' ), | |
| 597 | + 'pluginUrl' => esc_url_raw( untrailingslashit( OPENSTATION_URL ) ), | |
| 598 | + 'pluginVersion' => OPENSTATION_VERSION, | |
| 599 | + 'aboutFeedUrl' => esc_url_raw( | |
| 600 | + add_query_arg( | |
| 601 | + array( | |
| 602 | + 'action' => 'openstation_about_feed', | |
| 603 | + 'nonce' => wp_create_nonce( 'openstation_about_feed' ), | |
| 604 | + ), | |
| 605 | + admin_url( 'admin-ajax.php' ) | |
| 606 | + ) | |
| 607 | + ), | |
| 608 | + 'iframeBridgeUrl' => $lazy_bundle_url( 'iframe-bridge' ), | |
| 455 | 609 | // URL of the AI Assistant lazy bundle. The main bundle |
| 456 | - // ships a stub matching the public `wp.desktop.ai` API; the | |
| 610 | + // ships a stub matching the public `wp.os.ai` API; the | |
| 457 | 611 | // stub `<script>`-injects this URL the first time the user |
| 458 | 612 | // opens the assistant. Picking `.js` vs `.min.js` here keeps |
| 459 | 613 | // the SCRIPT_DEBUG gate server-side, matching iframeBridgeUrl. |
| 460 | - 'aiAssistantBundleUrl' => $lazy_bundle_url( 'ai-assistant' ), | |
| 461 | - // URL of the About-scene lazy bundle. The OS Settings → | |
| 462 | - // About tab loads this on first mount; ~25 kB PixiJS | |
| 463 | - // particle scene that would otherwise ship in the main | |
| 464 | - // bundle for every shell load. | |
| 465 | - 'aboutSceneBundleUrl' => $lazy_bundle_url( 'about-scene' ), | |
| 466 | - // URL of the OS Settings panel lazy bundle. Injected by | |
| 467 | - // the main bundle's `OsSettings.renderPanel()` stub on | |
| 468 | - // the user's first Settings open. Holds every section | |
| 469 | - // renderer + the `<wpd-*>` components only the panel | |
| 470 | - // uses, so nothing about Settings ships in | |
| 471 | - // `desktop.min.js` for users who never open it. | |
| 472 | - 'osSettingsPanelBundleUrl' => $lazy_bundle_url( 'os-settings-panel' ), | |
| 614 | + 'aiAssistantBundleUrl' => $lazy_bundle_url( 'ai-assistant' ), | |
| 473 | 615 | // URL of the shell-overlays lazy bundle. Pre-loaded by |
| 474 | 616 | // the main bundle after first paint so action-triggered |
| 475 | 617 | // overlays (toast, confirm dialog, context menus) feel |
| 476 | 618 | // instant the first time they fire. |
| 477 | - 'shellOverlaysBundleUrl' => $lazy_bundle_url( 'shell-overlays' ), | |
| 619 | + 'shellOverlaysBundleUrl' => $lazy_bundle_url( 'shell-overlays' ), | |
| 620 | + // The shell-bundle diet: features whose right moment is a | |
| 621 | + // user gesture (or a presence signal) ride their own | |
| 622 | + // bundles instead of the boot-critical `desktop[.min].js`. | |
| 623 | + // Each sentinel in the shell loads its bundle at that | |
| 624 | + // moment — see the entry file each bundle names. | |
| 625 | + 'fileDropBundleUrl' => $lazy_bundle_url( 'file-drop' ), | |
| 626 | + 'filesOverlaysBundleUrl' => $lazy_bundle_url( 'files-overlays' ), | |
| 627 | + 'notesBundleUrl' => $lazy_bundle_url( 'notes' ), | |
| 628 | + 'dockConstellationBundleUrl' => $lazy_bundle_url( 'dock-constellation' ), | |
| 629 | + 'windowLinkVisualsBundleUrl' => $lazy_bundle_url( 'window-link-visuals' ), | |
| 630 | + // Presence hint for the notes sentinel: a desktop with no | |
| 631 | + // notes skips the notes bundle AND the boot-time list | |
| 632 | + // request. Two id-only existence probes at most. | |
| 633 | + 'hasNotes' => function_exists( 'openstation_notes_user_has_any' ) | |
| 634 | + ? openstation_notes_user_has_any() | |
| 635 | + : false, | |
| 636 | + // URL of the full `<os-*>` component kit. The shell | |
| 637 | + // never loads this — its own bundles import the | |
| 638 | + // components they render. It exists for | |
| 639 | + // `wp.os.loadComponents()`, i.e. for plugin code that | |
| 640 | + // CANNOT import: a plugin shipped as a zip has no path | |
| 641 | + // to this repo at build time, so before this URL its | |
| 642 | + // only routes to a `<os-switch>` were to bundle a second | |
| 643 | + // copy or hand-roll one. Shipping the URL costs one | |
| 644 | + // string and keeps the SCRIPT_DEBUG choice server-side. | |
| 645 | + 'componentsBundleUrl' => $lazy_bundle_url( 'os-components' ), | |
| 646 | + // Mio — the desk companion. `mio` carries the | |
| 647 | + // appearance + physics (see `openstation_mio_config()`); | |
| 648 | + // `mioBundleUrl` is the lazy PixiJS bundle the shell | |
| 649 | + // controller injects the first time a user switches the | |
| 650 | + // Mio on from its dock tile. Shipping the URL | |
| 651 | + // unconditionally costs one short string and keeps the | |
| 652 | + // SCRIPT_DEBUG choice server-side, matching every other | |
| 653 | + // lazy bundle here. | |
| 654 | + // | |
| 655 | + // Both keys ship whether or not the user has Mio on, | |
| 656 | + // and that is the whole of its cost to a shell that doesn't: | |
| 657 | + // ~470 bytes gzipped of config, plus a URL. No script, no | |
| 658 | + // style, no PixiJS. The config has to be here rather than | |
| 659 | + // fetched on first toggle, or the `openstation_mio_config` | |
| 660 | + // filter would silently not apply until the next reload. | |
| 661 | + 'mio' => openstation_mio_config(), | |
| 662 | + 'mioBundleUrl' => $lazy_bundle_url( 'mio' ), | |
| 478 | 663 | // URL of the lazy window-system bundle (Stage 11). |
| 479 | 664 | // Holds the `Window` class and its DOM / pointer / tab / |
| 480 | - // chrome helpers — the single largest module in the pre- | |
| 481 | - // 0.8.4 main bundle. Loaded on first `windowManager.open()` | |
| 482 | - // / `openNew()` call (both async since 0.8.4); pre-loaded | |
| 665 | + // chrome helpers — the single largest module split out of | |
| 666 | + // the main bundle. Loaded on first `windowManager.open()` | |
| 667 | + // / `openNew()` call (both async); pre-loaded | |
| 483 | 668 | // by the shell after first paint when no session is being |
| 484 | 669 | // restored and no `openCurrentPage` will fire. |
| 485 | - 'windowSystemBundleUrl' => $lazy_bundle_url( 'window-system' ), | |
| 670 | + 'windowSystemBundleUrl' => $lazy_bundle_url( 'window-system' ), | |
| 671 | + // URL of the lazy phone-layer bundle. Injected by the main | |
| 672 | + // bundle only when the mode resolves to `mobile`, so a | |
| 673 | + // desktop never fetches it. `mode` carries the preference | |
| 674 | + // and breakpoints the first-paint head stamp already used, | |
| 675 | + // plus the server's default tab-bar pins — see | |
| 676 | + // `includes/mobile.php`. | |
| 677 | + 'mobileBundleUrl' => $lazy_bundle_url( 'mobile' ), | |
| 678 | + 'mode' => openstation_mode_config( get_current_user_id() ), | |
| 486 | 679 | // URL of the item-visibility-menu lazy bundle — the |
| 487 | 680 | // right-click "hide from dock / desktop" menu. Injected by |
| 488 | 681 | // the main bundle's loader shim on the first right-click. |
| 489 | - 'itemVisibilityMenuBundleUrl' => $lazy_bundle_url( 'item-visibility-menu' ), | |
| 682 | + 'itemVisibilityMenuBundleUrl' => $lazy_bundle_url( 'item-visibility-menu' ), | |
| 683 | + // URL of the workspace-wizard lazy bundle — the modal | |
| 684 | + // behind "Edit this workspace…". Injected by the main | |
| 685 | + // bundle's loader shim on first open. | |
| 686 | + 'workspaceWizardBundleUrl' => $lazy_bundle_url( 'workspace-wizard' ), | |
| 687 | + // Server-side view of the workspace templates, so a plugin | |
| 688 | + // can add or drop one from PHP. The client merges these | |
| 689 | + // with its own built-ins by id — see | |
| 690 | + // `src/workspaces/server-sync.ts`. | |
| 691 | + 'workspacePresets' => openstation_workspace_presets(), | |
| 490 | 692 | // URL of the release-card lazy bundle — the vinyl core- |
| 491 | 693 | // update announcement. Injected by `maybeShowUpdate()` only |
| 492 | 694 | // when a core update is actually pending. |
| 493 | - 'releaseCardBundleUrl' => $lazy_bundle_url( 'release-card' ), | |
| 494 | - 'restNonce' => wp_create_nonce( 'wp_rest' ), | |
| 495 | - 'osSettings' => desktop_mode_get_os_settings( get_current_user_id() ), | |
| 496 | - 'osSettingsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/os-settings' ) ), | |
| 497 | - 'seenIntros' => desktop_mode_get_seen_intros( get_current_user_id() ), | |
| 498 | - 'seenIntrosUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/intros' ) ), | |
| 499 | - // Sticky notes ride on Gutenberg's Guidelines experiment | |
| 500 | - // (the `wp_guideline` CPT + `wp_guideline_type` taxonomy). | |
| 501 | - // When that experiment isn't active the `wp/v2/guidelines` | |
| 502 | - // + `wp/v2/wp_guideline_type` probes 404 — harmless but | |
| 503 | - // noisy — so the shell skips booting the layer entirely. | |
| 504 | - 'stickyNotes' => array( | |
| 505 | - 'available' => desktop_mode_sticky_notes_is_available(), | |
| 506 | - ), | |
| 507 | - 'aiSearchUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/ai/search' ) ), | |
| 508 | - 'aiSearchStreamUrl' => esc_url_raw( add_query_arg( 'action', 'desktop_mode_ai_search_stream', admin_url( 'admin-ajax.php' ) ) ), | |
| 695 | + 'releaseCardBundleUrl' => $lazy_bundle_url( 'release-card' ), | |
| 696 | + 'restNonce' => wp_create_nonce( 'wp_rest' ), | |
| 697 | + // Non-empty when the shell was asked to paint exactly one | |
| 698 | + // window and nothing else. See `OPENSTATION_SOLO_FLAG`. | |
| 699 | + 'soloWindow' => openstation_solo_window_id(), | |
| 700 | + 'osSettings' => openstation_get_os_settings( get_current_user_id() ), | |
| 701 | + 'osSettingsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/os-settings' ) ), | |
| 702 | + 'seenIntros' => openstation_get_seen_intros( get_current_user_id() ), | |
| 703 | + 'seenIntrosUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/intros' ) ), | |
| 704 | + // True only for a user migration 5 flagged as a Desktop Mode | |
| 705 | + // user from before the rename, who hasn't dismissed the | |
| 706 | + // announcement yet. Same value that gated `os-announce` | |
| 707 | + // above; the dialog cannot paint without that stylesheet, so | |
| 708 | + // the two must not diverge. | |
| 709 | + 'rebrandNotice' => $show_rebrand_notice, | |
| 710 | + 'aiSearchUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/ai/search' ) ), | |
| 509 | 711 | // AI assistant availability + per-user toggle. Drives whether the |
| 510 | 712 | // Cmd+K palette and admin-bar icon appear, and the setup placeholder. |
| 511 | - 'aiAssistant' => function_exists( 'desktop_mode_ai_assistant_config' ) | |
| 512 | - ? desktop_mode_ai_assistant_config() | |
| 713 | + 'aiAssistant' => function_exists( 'openstation_ai_assistant_config' ) | |
| 714 | + ? openstation_ai_assistant_config() | |
| 513 | 715 | : null, |
| 514 | 716 | // Lets the Features tab re-check provider availability without a |
| 515 | 717 | // reload after a connector is configured in Settings → Connectors. |
| 516 | - 'aiStatusUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/ai/status' ) ), | |
| 517 | - 'extendedOptions' => current_user_can( 'manage_options' ) ? desktop_mode_get_extended_options() : null, | |
| 518 | - 'extendedOptionsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/extended-options' ) ), | |
| 718 | + 'aiStatusUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/ai/status' ) ), | |
| 719 | + 'extendedOptions' => current_user_can( 'manage_options' ) ? openstation_get_extended_options() : null, | |
| 720 | + 'extendedOptionsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/extended-options' ) ), | |
| 519 | 721 | // Site-wide games kill switch (Extended options). Exposed to |
| 520 | 722 | // every user — the shell skips the challenges Heartbeat |
| 521 | 723 | // channel when the framework is off. |
| 522 | - 'gamesEnabled' => desktop_mode_games_enabled(), | |
| 523 | - // Comments-window AI moderation toggle — surfaced at the | |
| 524 | - // shell level so the OS Settings → Features tab can render | |
| 525 | - // the toggle without depending on the Comments window | |
| 526 | - // being registered for this user. URL is the same | |
| 527 | - // endpoint the comments-window config exposes; state is | |
| 528 | - // `null` for non-admins (the UI hides the row entirely). | |
| 529 | - 'commentsAiUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/ai-settings' ) ), | |
| 530 | - // Non-null only for admins on a site where the Core AI stack is | |
| 531 | - // present. Comment scoring routes through the AI Client (WP 7.0+), | |
| 532 | - // so on older WordPress the whole row is hidden — same as the | |
| 533 | - // assistant toggle — rather than shown disabled pointing at a | |
| 534 | - // Settings → Connectors screen that doesn't exist there. | |
| 535 | - 'commentsAi' => ( | |
| 536 | - current_user_can( 'manage_options' ) | |
| 537 | - && function_exists( 'desktop_mode_ai_is_available' ) | |
| 538 | - && desktop_mode_ai_is_available() | |
| 539 | - ) | |
| 540 | - ? array( | |
| 541 | - 'enabled' => function_exists( 'desktop_mode_comments_ai_is_enabled' ) | |
| 542 | - ? desktop_mode_comments_ai_is_enabled() | |
| 543 | - : false, | |
| 544 | - 'providerConfigured' => function_exists( 'desktop_mode_comments_ai_provider_configured' ) | |
| 545 | - ? desktop_mode_comments_ai_provider_configured() | |
| 546 | - : false, | |
| 547 | - ) | |
| 548 | - : null, | |
| 549 | - 'currentUserIsAdmin' => current_user_can( 'manage_options' ), | |
| 550 | - 'portalUrl' => esc_url( desktop_mode_portal_url() ), | |
| 551 | - 'fromPortal' => $from_portal, | |
| 552 | - 'fromPortalIntent' => $from_portal_intent, | |
| 553 | - 'pwa' => array( | |
| 554 | - 'manifestUrl' => esc_url_raw( desktop_mode_pwa_manifest_url() ), | |
| 555 | - 'swUrl' => esc_url_raw( desktop_mode_pwa_sw_url() ), | |
| 724 | + 'gamesEnabled' => openstation_games_enabled(), | |
| 725 | + 'currentUserIsAdmin' => current_user_can( 'manage_options' ), | |
| 726 | + // Null on single-site installs; its `networkAdmin` null | |
| 727 | + // without `manage_network`, which is what keeps the dock | |
| 728 | + // tile from registering. | |
| 729 | + 'multisite' => openstation_multisite_payload(), | |
| 730 | + 'portalUrl' => esc_url( openstation_portal_url() ), | |
| 731 | + 'fromPortal' => $from_portal, | |
| 732 | + 'fromPortalIntent' => $from_portal_intent, | |
| 733 | + // One-shot like the boot target: a switch from another | |
| 734 | + // site's overview lands in this one's. | |
| 735 | + 'landInOverview' => openstation_shell_lands_in_overview(), | |
| 736 | + 'arrivalDirection' => openstation_shell_arrival_direction(), | |
| 737 | + 'hopLinkOffer' => function_exists( 'openstation_network_link_offer' ) ? openstation_network_link_offer() : null, | |
| 738 | + 'pwa' => array( | |
| 739 | + 'manifestUrl' => esc_url_raw( openstation_pwa_manifest_url() ), | |
| 740 | + 'swUrl' => esc_url_raw( openstation_pwa_sw_url() ), | |
| 741 | + // Extensionless retry target for hosts whose nginx 404s | |
| 742 | + // virtual .js paths before WordPress runs (WordPress.com). | |
| 743 | + 'swFallbackUrl' => esc_url_raw( openstation_pwa_sw_fallback_url() ), | |
| 744 | + // The site's home path — the scope the registration | |
| 745 | + // asks for, so a subdirectory network's sites each get | |
| 746 | + // their own worker instead of fighting over the root. | |
| 747 | + 'swScope' => openstation_pwa_sw_scope(), | |
| 748 | + // The worker's per-user flags, computed HERE rather than | |
| 749 | + // baked into the served `sw.js`. | |
| 750 | + // | |
| 751 | + // A service worker is origin-wide but these are per-user | |
| 752 | + // preferences, so putting them in the script bytes made | |
| 753 | + // the body differ between an anonymous and a logged-in | |
| 754 | + // request — and any in-scope logged-out navigation then | |
| 755 | + // installed a "new" worker, which at the time reloaded | |
| 756 | + // the shell. The bytes are identical for everyone now; | |
| 757 | + // the shell posts these to the worker at boot. | |
| 758 | + // | |
| 759 | + // Computed server-side, not read from the settings | |
| 760 | + // snapshot client-side, because | |
| 761 | + // `openstation_pwa_admin_asset_cache_enabled()` applies | |
| 762 | + // the `openstation_pwa_admin_asset_cache` filter — an | |
| 763 | + // operator's site-wide veto has to keep working. | |
| 764 | + 'swConfig' => array( | |
| 765 | + 'adminAssetCache' => (bool) openstation_pwa_admin_asset_cache_enabled(), | |
| 766 | + 'windowPrewarm' => ! empty( openstation_get_os_settings( get_current_user_id() )['windowPrewarmEnabled'] ), | |
| 767 | + ), | |
| 768 | + // The build this shell document belongs to. When a new | |
| 769 | + // worker takes over mid-session the shell asks it for | |
| 770 | + // the stamp it was served with and compares; only a | |
| 771 | + // difference — the shell's own files changed on the | |
| 772 | + // server — offers the user a reload. Never automatic. | |
| 773 | + 'shellBuild' => openstation_shell_build_stamp(), | |
| 556 | 774 | 'stateUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/pwa-state' ) ), |
| 557 | - 'state' => desktop_mode_pwa_get_user_state( get_current_user_id() ), | |
| 775 | + 'state' => openstation_pwa_get_user_state( get_current_user_id() ), | |
| 558 | 776 | // Mirrors the manifest's `name` field — used by the |
| 559 | 777 | // install pill so the button reads "Install <site>" |
| 560 | 778 | // rather than "Install <current page>" (which would |
| 561 | 779 | // be misleading: we install the whole site as an |
| @@ -561,31 +779,113 @@ | ||
| 561 | 779 | // be misleading: we install the whole site as an |
| 562 | 780 | // app, not the dashboard window the user happens to |
| 563 | 781 | // be viewing). |
| 564 | 782 | 'appName' => get_bloginfo( 'name' ), |
| 565 | - // Operators set the `desktop_mode_pwa_force_replace_sw` | |
| 783 | + // Operators set the `openstation_pwa_force_replace_sw` | |
| 566 | 784 | // filter to `true` when another root-scope service |
| 567 | - // worker on the origin is blocking desktop-mode | |
| 785 | + // worker on the origin is blocking openstation | |
| 568 | 786 | // installability (foreign-SW guard in |
| 569 | 787 | // `src/pwa/sw-register.ts`). Default `false` preserves |
| 570 | 788 | // the polite behaviour where we yield to existing PWAs. |
| 571 | - 'forceReplaceSw' => desktop_mode_pwa_force_replace_sw(), | |
| 789 | + 'forceReplaceSw' => openstation_pwa_force_replace_sw(), | |
| 572 | 790 | ), |
| 791 | + // Ordered Core command-palette asset manifest, replayed on | |
| 792 | + // first palette invocation. `null` on pre-6.9 sites. | |
| 793 | + 'commandPalette' => $command_palette, | |
| 794 | + // Stylesheets for shell surfaces that render on demand — | |
| 795 | + // the Preferences panel, the AI assistant, the bug-report | |
| 796 | + // window. None of them is a server-registered native | |
| 797 | + // window (they are built client-side by the shell | |
| 798 | + // bundle), so the `styles` companion mechanism can't | |
| 799 | + // carry their CSS; instead the shell injects each sheet | |
| 800 | + // the first time its surface opens, via | |
| 801 | + // `ensureDeferredStyle()` in `src/deferred-styles.ts`. | |
| 802 | + // Same resolved shape a native window's `styleUrl` / | |
| 803 | + // `styleInline` travels in. | |
| 804 | + // Which of the `deferredStyles` entries a game needs. | |
| 805 | + // `launchGame()` injects these before the window paints. | |
| 806 | + 'gameStyleHandles' => function_exists( 'openstation_games_style_handles' ) | |
| 807 | + ? openstation_games_style_handles() | |
| 808 | + : array(), | |
| 809 | + 'deferredStyles' => openstation_build_deferred_styles( | |
| 810 | + array_merge( | |
| 811 | + array( | |
| 812 | + 'desktop-mode-ai-assistant', | |
| 813 | + 'desktop-mode-bug-report', | |
| 814 | + // The explorer's shared sheet. It rides the WP | |
| 815 | + // Explorer APP as a companion style, but the | |
| 816 | + // desktop FOLDER window paints its preview pane | |
| 817 | + // with the same `os-my-wordpress__*` classes and | |
| 818 | + // — being a native window opened straight from | |
| 819 | + // JS — carries no companion styles of its own. | |
| 820 | + // Without this, the pane rendered unstyled until | |
| 821 | + // the explorer had been opened once in the | |
| 822 | + // session. | |
| 823 | + 'desktop-mode-my-wordpress', | |
| 824 | + ), | |
| 825 | + // The Games sheets. They also ride the hub window as | |
| 826 | + // companion styles, but a game is reachable without | |
| 827 | + // the hub — the challenge toast, solo mode, and | |
| 828 | + // `wp.os.games.launch()` all land in `launchGame()` | |
| 829 | + // with no hub window in the tab. Listing them here | |
| 830 | + // costs a URL each in the boot config and no CSS | |
| 831 | + // until `launchGame()` asks. | |
| 832 | + function_exists( 'openstation_games_style_handles' ) | |
| 833 | + ? openstation_games_style_handles() | |
| 834 | + : array() | |
| 835 | + ) | |
| 836 | + ), | |
| 573 | 837 | ) |
| 574 | 838 | ); |
| 575 | 839 | |
| 576 | - wp_localize_script( 'desktop-mode', 'desktopModeConfig', $config ); | |
| 840 | + wp_localize_script( 'openstation', 'openStationConfig', $config ); | |
| 577 | 841 | |
| 578 | 842 | /** |
| 579 | - * Fires when desktop mode assets are enqueued. | |
| 580 | - * | |
| 581 | - * @since 0.1.0 | |
| 843 | + * Fires when OpenStation assets are enqueued. | |
| 582 | 844 | */ |
| 583 | - do_action( 'desktop_mode_mode_init' ); | |
| 845 | + do_action( 'openstation_mode_init' ); | |
| 584 | 846 | } |
| 585 | -add_action( 'admin_enqueue_scripts', 'desktop_mode_enqueue_assets' ); | |
| 847 | +add_action( 'admin_enqueue_scripts', 'openstation_enqueue_assets' ); | |
| 586 | 848 | |
| 587 | 849 | /** |
| 850 | + * Keep Core's boot-time command-palette enqueue off shell pages. | |
| 851 | + * | |
| 852 | + * WordPress 7.0 hooks `wp_enqueue_command_palette_assets()` on | |
| 853 | + * `admin_enqueue_scripts` by default, which puts the palette's whole | |
| 854 | + * dependency chain — the Gutenberg runtime, ~800 KB gzipped — on | |
| 855 | + * every admin page. On a SHELL page that is pure dead weight: the | |
| 856 | + * shell suppresses Core's palette unconditionally (the ⌘K keystroke | |
| 857 | + * and the admin-bar icon both route to the shell's own palette), so | |
| 858 | + * the runtime it powers can never be shown. Unhooking here lets the | |
| 859 | + * deferred manifest (`openstation_build_command_palette_assets_payload()`) | |
| 860 | + * capture the chain instead, and the shell loads it on the first | |
| 861 | + * palette invocation. | |
| 862 | + * | |
| 863 | + * Deliberately scoped: classic-mode requests keep Core's default, | |
| 864 | + * because a classic page is Core's own UI where Core's palette is the | |
| 865 | + * right one. | |
| 866 | + * | |
| 867 | + * Windows are handled separately by | |
| 868 | + * {@see openstation_chromeless_should_trim_command_palette()} in | |
| 869 | + * `includes/render/chromeless-trim.php` — same idea, but it has to | |
| 870 | + * drop the whole palette *family* rather than just unhook Core's | |
| 871 | + * callback, and it exempts block-editor screens. Unhooking alone is | |
| 872 | + * not enough there: a third-party palette extension that declares | |
| 873 | + * `wp-commands` keeps the entire chain queued as its dependency. | |
| 874 | + * | |
| 875 | + * Priority 0, ahead of Core's default 10, so the removal lands | |
| 876 | + * before the callback fires. On WP 6.9 (function exists, no default | |
| 877 | + * hook) the `remove_action()` is a harmless no-op. | |
| 878 | + */ | |
| 879 | +function openstation_defer_core_command_palette() { | |
| 880 | + if ( ! openstation_is_shell_request() ) { | |
| 881 | + return; | |
| 882 | + } | |
| 883 | + remove_action( 'admin_enqueue_scripts', 'wp_enqueue_command_palette_assets' ); | |
| 884 | +} | |
| 885 | +add_action( 'admin_enqueue_scripts', 'openstation_defer_core_command_palette', 0 ); | |
| 886 | + | |
| 887 | +/** | |
| 588 | 888 | * Emits `<link rel="preload">` hints for the shell's critical-path |
| 589 | 889 | * assets so the browser starts fetching them as soon as it parses |
| 590 | 890 | * the document `<head>`. |
| 591 | 891 | * |
| @@ -618,9 +918,9 @@ | ||
| 618 | 918 | * later (often beyond the ~3s window Chrome allows a `preload` before it |
| 619 | 919 | * warns "preloaded but not used in time"), so `prefetch` keeps the early |
| 620 | 920 | * low-priority cache fill without the must-use-now contract. |
| 621 | 921 | * |
| 622 | - * Plugins can extend the hint list via the `desktop_mode_preload_hints` | |
| 922 | + * Plugins can extend the hint list via the `openstation_preload_hints` | |
| 623 | 923 | * filter — e.g. a settings tab whose bundle the user opens on every |
| 624 | 924 | * visit can opt its own URL into the preload phase. |
| 625 | 925 | * |
| 626 | 926 | * Same-origin resources only — no `crossorigin` attribute. CDN hosts |
| @@ -626,27 +926,20 @@ | ||
| 626 | 926 | * Same-origin resources only — no `crossorigin` attribute. CDN hosts |
| 627 | 927 | * that serve `wp-content/plugins/` from a different origin should |
| 628 | 928 | * supply absolute URLs through the filter; in that case the consumer |
| 629 | 929 | * is responsible for the `crossorigin` semantics. |
| 630 | - * | |
| 631 | - * @since 0.8.9 | |
| 632 | 930 | */ |
| 633 | -function desktop_mode_print_preload_hints() { | |
| 634 | - if ( | |
| 635 | - ! is_admin() | |
| 636 | - || ! desktop_mode_is_enabled() | |
| 637 | - || desktop_mode_is_chromeless_request() | |
| 638 | - || desktop_mode_is_classic_request() | |
| 639 | - ) { | |
| 931 | +function openstation_print_preload_hints() { | |
| 932 | + if ( ! openstation_is_shell_request() ) { | |
| 640 | 933 | return; |
| 641 | 934 | } |
| 642 | 935 | |
| 643 | - $suffix = desktop_mode_asset_suffix(); | |
| 936 | + $suffix = openstation_asset_suffix(); | |
| 644 | 937 | |
| 645 | 938 | $build_url = static function ( $relative ) { |
| 646 | - $path = DESKTOP_MODE_DIR . $relative; | |
| 647 | - $ver = file_exists( $path ) ? (string) filemtime( $path ) : DESKTOP_MODE_VERSION; | |
| 648 | - return DESKTOP_MODE_URL . $relative . '?ver=' . $ver; | |
| 939 | + $path = OPENSTATION_DIR . $relative; | |
| 940 | + $ver = file_exists( $path ) ? (string) filemtime( $path ) : OPENSTATION_VERSION; | |
| 941 | + return OPENSTATION_URL . $relative . '?ver=' . $ver; | |
| 649 | 942 | }; |
| 650 | 943 | |
| 651 | 944 | $hints = array( |
| 652 | 945 | // Critical path — consumed on this very page load (the footer |
| @@ -680,8 +973,21 @@ | ||
| 680 | 973 | 'rel' => 'prefetch', |
| 681 | 974 | ), |
| 682 | 975 | ); |
| 683 | 976 | |
| 977 | + // The phone layer is needed at boot on a phone and never on a | |
| 978 | + // desktop; the server cannot see the viewport, so the user agent | |
| 979 | + // decides whether the hint is worth its bytes. A wrong guess costs | |
| 980 | + // one low-priority fetch, never a wrong layout — the stamp and the | |
| 981 | + // bundle loader read the real viewport. | |
| 982 | + if ( openstation_mode_hint_is_mobile( get_current_user_id() ) ) { | |
| 983 | + $hints[] = array( | |
| 984 | + 'href' => $build_url( 'assets/js/mobile' . $suffix . '.js' ), | |
| 985 | + 'as' => 'script', | |
| 986 | + 'rel' => 'prefetch', | |
| 987 | + ); | |
| 988 | + } | |
| 989 | + | |
| 684 | 990 | /** |
| 685 | 991 | * Filters the list of resource preload hints emitted in `<head>`. |
| 686 | 992 | * |
| 687 | 993 | * Each entry is a `{ 'href' => string, 'as' => string, |
| @@ -691,16 +997,13 @@ | ||
| 691 | 997 | * back to `preload`. Unrecognized entries are silently skipped — keep |
| 692 | 998 | * the contract permissive so a misconfigured plugin can't tank first |
| 693 | 999 | * paint. |
| 694 | 1000 | * |
| 695 | - * @since 0.8.9 | |
| 696 | - * @since 0.9.1 Entries may carry a `rel` key (`preload` | `prefetch`). | |
| 697 | - * | |
| 698 | 1001 | * @param array $hints Default hints (main bundle + base CSS as |
| 699 | 1002 | * `preload`; window-system + shell-overlays as |
| 700 | 1003 | * `prefetch`). |
| 701 | 1004 | */ |
| 702 | - $hints = apply_filters( 'desktop_mode_preload_hints', $hints ); | |
| 1005 | + $hints = apply_filters( 'openstation_preload_hints', $hints ); | |
| 703 | 1006 | |
| 704 | 1007 | if ( ! is_array( $hints ) ) { |
| 705 | 1008 | return; |
| 706 | 1009 | } |
| @@ -728,12 +1031,12 @@ | ||
| 728 | 1031 | esc_url( $href ) |
| 729 | 1032 | ); |
| 730 | 1033 | } |
| 731 | 1034 | } |
| 732 | -add_action( 'admin_print_styles', 'desktop_mode_print_preload_hints', 1 ); | |
| 1035 | +add_action( 'admin_print_styles', 'openstation_print_preload_hints', 1 ); | |
| 733 | 1036 | |
| 734 | 1037 | /** |
| 735 | - * Defers loading of non-critical desktop-mode stylesheets so they | |
| 1038 | + * Defers loading of non-critical openstation stylesheets so they | |
| 736 | 1039 | * don't block first paint. |
| 737 | 1040 | * |
| 738 | 1041 | * Three stylesheets in the default enqueue list are only needed |
| 739 | 1042 | * after a user interaction — `dock-peek` (mouseover a dock tile), |
| @@ -757,14 +1060,12 @@ | ||
| 757 | 1060 | * before the user clicks anything that needs them. The |
| 758 | 1061 | * `<noscript>` fallback restores critical-path behavior for JS-off |
| 759 | 1062 | * browsers, so accessibility isn't degraded. |
| 760 | 1063 | * |
| 761 | - * Filterable via `desktop_mode_deferred_styles` so plugins can opt | |
| 1064 | + * Filterable via `openstation_deferred_styles` so plugins can opt | |
| 762 | 1065 | * their own non-critical stylesheets in (or pull a built-in out). |
| 763 | 1066 | * Chromeless iframes are skipped — their CSS pipeline is separate. |
| 764 | 1067 | * |
| 765 | - * @since 0.8.9 | |
| 766 | - * | |
| 767 | 1068 | * @param string $html The original <link> tag HTML. |
| 768 | 1069 | * @param string $handle The stylesheet handle WP is printing. |
| 769 | 1070 | * @param string $href The full URL of the stylesheet. |
| 770 | 1071 | * @param string $media The media attribute value WP resolved. |
| @@ -769,22 +1070,22 @@ | ||
| 769 | 1070 | * @param string $href The full URL of the stylesheet. |
| 770 | 1071 | * @param string $media The media attribute value WP resolved. |
| 771 | 1072 | * @return string Possibly-rewritten tag. |
| 772 | 1073 | */ |
| 773 | -function desktop_mode_defer_non_critical_styles( $html, $handle, $href, $media ) { | |
| 1074 | +function openstation_defer_non_critical_styles( $html, $handle, $href, $media ) { | |
| 774 | 1075 | // Cheap gates first — `style_loader_tag` fires once per enqueued |
| 775 | 1076 | // stylesheet on EVERY admin page (frontend doesn't go through |
| 776 | - // this filter, but admin does, including pages where desktop mode | |
| 777 | - // is disabled). The deferred handles only ship when desktop mode | |
| 1077 | + // this filter, but admin does, including pages where OpenStation | |
| 1078 | + // is disabled). The deferred handles only ship when OpenStation | |
| 778 | 1079 | // is active, so the in_array check below would always miss on |
| 779 | 1080 | // classic-only admin pages — but the `apply_filters` call still |
| 780 | 1081 | // builds an array and walks subscribers per stylesheet. Short- |
| 781 | 1082 | // circuit on the cheap helper checks (`is_admin` / enabled / |
| 782 | - // chromeless) so non-desktop-mode users pay nothing. | |
| 783 | - if ( ! desktop_mode_is_enabled() ) { | |
| 1083 | + // chromeless) so non-openstation users pay nothing. | |
| 1084 | + if ( ! openstation_is_enabled() ) { | |
| 784 | 1085 | return $html; |
| 785 | 1086 | } |
| 786 | - if ( desktop_mode_is_chromeless_request() ) { | |
| 1087 | + if ( openstation_is_chromeless_request() ) { | |
| 787 | 1088 | return $html; |
| 788 | 1089 | } |
| 789 | 1090 | |
| 790 | 1091 | /** |
| @@ -793,20 +1094,18 @@ | ||
| 793 | 1094 | * their own non-critical stylesheets here, or pull a built-in |
| 794 | 1095 | * out (e.g. a plugin that surfaces the AI assistant on every |
| 795 | 1096 | * page might want to keep its CSS critical-path). |
| 796 | 1097 | * |
| 797 | - * @since 0.8.9 | |
| 798 | - * | |
| 799 | 1098 | * @param string[] $handles Default deferred handles. |
| 800 | 1099 | */ |
| 801 | 1100 | $deferred = apply_filters( |
| 802 | - 'desktop_mode_deferred_styles', | |
| 1101 | + 'openstation_deferred_styles', | |
| 803 | 1102 | array( |
| 804 | - 'desktop-mode-dock-peek', | |
| 1103 | + 'os-dock-peek', | |
| 1104 | + 'os-openstation-layout', | |
| 805 | 1105 | 'desktop-mode-ai-assistant', |
| 806 | 1106 | 'desktop-mode-bug-report', |
| 807 | - 'desktop-mode-window-overview', | |
| 808 | - 'desktop-mode-os-settings', | |
| 1107 | + 'os-window-overview', | |
| 809 | 1108 | ) |
| 810 | 1109 | ); |
| 811 | 1110 | |
| 812 | 1111 | if ( ! in_array( $handle, (array) $deferred, true ) ) { |
| @@ -817,22 +1116,22 @@ | ||
| 817 | 1116 | $id = $handle . '-css'; |
| 818 | 1117 | |
| 819 | 1118 | // Two contexts, two escapers for the same `$resolved_media` value: |
| 820 | 1119 | // |
| 821 | - // - `%3$s` lands inside a JS string literal inside the HTML | |
| 822 | - // `onload="…"` attribute (`this.media='%3$s'`). `esc_attr` | |
| 823 | - // escapes `"` and `&` but NOT single quotes, so a media | |
| 824 | - // value containing `'` would break out of the JS string. | |
| 825 | - // `esc_js` is the correct escaper for "string literal inside | |
| 826 | - // an event-handler attribute" — escapes single quotes, double | |
| 827 | - // quotes, backslashes, newlines. Today `$resolved_media` | |
| 828 | - // comes from `wp_enqueue_style()`'s `$media` parameter (always | |
| 829 | - // a CSS media type / query produced by WordPress core), so | |
| 830 | - // this is pure defense-in-depth, but the cost is one extra | |
| 831 | - // function call. | |
| 1120 | + // - `%3$s` lands inside a JS string literal inside the HTML | |
| 1121 | + // `onload="…"` attribute (`this.media='%3$s'`). `esc_attr` | |
| 1122 | + // escapes `"` and `&` but NOT single quotes, so a media | |
| 1123 | + // value containing `'` would break out of the JS string. | |
| 1124 | + // `esc_js` is the correct escaper for "string literal inside | |
| 1125 | + // an event-handler attribute" — escapes single quotes, double | |
| 1126 | + // quotes, backslashes, newlines. Today `$resolved_media` | |
| 1127 | + // comes from `wp_enqueue_style()`'s `$media` parameter (always | |
| 1128 | + // a CSS media type / query produced by WordPress core), so | |
| 1129 | + // this is pure defense-in-depth, but the cost is one extra | |
| 1130 | + // function call. | |
| 832 | 1131 | // |
| 833 | - // - `%4$s` lands inside an HTML attribute in the `<noscript>` | |
| 834 | - // fallback (`media='%4$s'`). That's standard `esc_attr`. | |
| 1132 | + // - `%4$s` lands inside an HTML attribute in the `<noscript>` | |
| 1133 | + // fallback (`media='%4$s'`). That's standard `esc_attr`. | |
| 835 | 1134 | // |
| 836 | 1135 | // phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet -- This filter rewrites a tag WordPress is in the process of emitting for an already-registered+enqueued stylesheet handle; the linter doesn't trace the `style_loader_tag` filter context, so the raw <link rel="stylesheet"> output is a false-positive. |
| 837 | 1136 | $markup = sprintf( |
| 838 | 1137 | '<link rel=\'stylesheet\' id=\'%1$s\' href=\'%2$s\' media=\'print\' onload="this.media=\'%3$s\'; this.onload=null;" />' . "\n" . |
| @@ -845,19 +1144,19 @@ | ||
| 845 | 1144 | // phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet |
| 846 | 1145 | |
| 847 | 1146 | return $markup; |
| 848 | 1147 | } |
| 849 | -add_filter( 'style_loader_tag', 'desktop_mode_defer_non_critical_styles', 10, 4 ); | |
| 1148 | +add_filter( 'style_loader_tag', 'openstation_defer_non_critical_styles', 10, 4 ); | |
| 850 | 1149 | |
| 851 | 1150 | /** |
| 852 | 1151 | * Build the admin-menu command map (name → URL) and expose it on |
| 853 | - * `window.__desktopModeMenuCommands`. The shell command harvester | |
| 1152 | + * `window.__openStationMenuCommands`. The shell command harvester | |
| 854 | 1153 | * (`src/commands/shell-harvester.ts`) reads this slot to resolve URLs |
| 855 | 1154 | * for "Go to: …" commands whose JS callbacks |
| 856 | 1155 | * (`document.location = menuCommand.url`) close over a variable URL |
| 857 | 1156 | * we can't extract from source. Without this map those commands |
| 858 | 1157 | * either get skipped (no URL recoverable) or — if the location |
| 859 | - * shadow misses — navigate the SHELL out of desktop mode. | |
| 1158 | + * shadow misses — navigate the SHELL out of OpenStation. | |
| 860 | 1159 | * |
| 861 | 1160 | * Mirrors what WordPress core's `wp_enqueue_command_palette_assets()` |
| 862 | 1161 | * builds for `wp.coreCommands.initializeCommandPalette(...)`. We |
| 863 | 1162 | * duplicate the logic here (instead of monkey-patching the JS init |
| @@ -866,15 +1165,13 @@ | ||
| 866 | 1165 | * same handle) and ship the result through `wp_add_inline_script` on |
| 867 | 1166 | * our own bundle handle. That decouples us entirely from WP's command- |
| 868 | 1167 | * palette mount timing. |
| 869 | 1168 | * |
| 870 | - * @since 0.8.4 | |
| 871 | - * | |
| 872 | 1169 | * @global array $menu |
| 873 | 1170 | * @global array $submenu |
| 874 | 1171 | * @return array<int, array{label:string, url:string, name:string}> |
| 875 | 1172 | */ |
| 876 | -function desktop_mode_build_command_menu_map() { | |
| 1173 | +function openstation_build_command_menu_map() { | |
| 877 | 1174 | global $menu, $submenu, $_parent_pages; |
| 878 | 1175 | if ( ! is_array( $menu ) ) { |
| 879 | 1176 | return array(); |
| 880 | 1177 | } |
| @@ -927,9 +1224,9 @@ | ||
| 927 | 1224 | // exception is URL-style slugs referencing a real admin file |
| 928 | 1225 | // (ACF's 'edit.php?post_type=acf-field-group' — also a |
| 929 | 1226 | // registered page) — those stay direct links, matching |
| 930 | 1227 | // classic admin's menu-header.php. |
| 931 | - if ( ( ! isset( $_parent_pages[ $menu_slug ] ) || desktop_mode_is_admin_file_slug( $menu_slug ) ) && ( preg_match( '/\.php($|\?)/', $menu_slug ) || wp_http_validate_url( $menu_slug ) ) ) { | |
| 1228 | + if ( ( ! isset( $_parent_pages[ $menu_slug ] ) || openstation_is_admin_file_slug( $menu_slug ) ) && ( preg_match( '/\.php($|\?)/', $menu_slug ) || wp_http_validate_url( $menu_slug ) ) ) { | |
| 932 | 1229 | $menu_url = $menu_slug; |
| 933 | 1230 | } elseif ( ! empty( menu_page_url( $menu_slug, false ) ) ) { |
| 934 | 1231 | $menu_url = menu_page_url( $menu_slug, false ); |
| 935 | 1232 | } |
| @@ -952,9 +1249,9 @@ | ||
| 952 | 1249 | $submenu_slug = $submenu_item[2]; |
| 953 | 1250 | $submenu_url = ''; |
| 954 | 1251 | // Same registered-page vs admin-file rule as the |
| 955 | 1252 | // top-level loop. |
| 956 | - if ( ( ! isset( $_parent_pages[ $submenu_slug ] ) || desktop_mode_is_admin_file_slug( $submenu_slug ) ) && ( preg_match( '/\.php($|\?)/', $submenu_slug ) || wp_http_validate_url( $submenu_slug ) ) ) { | |
| 1253 | + if ( ( ! isset( $_parent_pages[ $submenu_slug ] ) || openstation_is_admin_file_slug( $submenu_slug ) ) && ( preg_match( '/\.php($|\?)/', $submenu_slug ) || wp_http_validate_url( $submenu_slug ) ) ) { | |
| 957 | 1254 | $submenu_url = $submenu_slug; |
| 958 | 1255 | } elseif ( ! empty( menu_page_url( $submenu_slug, false ) ) ) { |
| 959 | 1256 | $submenu_url = menu_page_url( $submenu_slug, false ); |
| 960 | 1257 | } |