PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.11
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.11
1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 All 35 releases
← All changes | includes/registries/native-windows.php +626 -267 0.9.8 → 1.1.11 View file →
@@ -1,20 +1,20 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Native windows registry.
3 + * OpenStation — Native windows registry.
4 4 *
5 5 * The largest of the five components.php registries — owns:
6 6 *
7 - * - `desktop_mode_register_window()` — plugin-author API
8 - * - `desktop_mode_native_window_registry()` — internal store
9 - * - `desktop_mode_native_window_allowed_html()` — wp_kses
7 + * - `openstation_register_window()` — plugin-author API
8 + * - `openstation_native_window_registry()` — internal store
9 + * - `openstation_native_window_allowed_html()` — wp_kses
10 10 * allowlist for `<template>` payloads
11 - * - `desktop_mode_build_native_window_template_html()` —
11 + * - `openstation_build_native_window_template_html()` —
12 12 * wraps the registered template callback in tabs markup
13 13 * when the window has multiple registered tabs
14 - * - `desktop_mode_enqueue_native_window_scripts()` — enqueue
14 + * - `openstation_enqueue_native_window_scripts()` — enqueue
15 15 * hook that ships every registered window's script handle
16 - * - `desktop_mode_render_native_window_templates()` — renders
16 + * - `openstation_render_native_window_templates()` — renders
17 17 * the `<template>` elements the shell clones
18 18 *
19 19 * Extracted from `components.php` during the architecture-0.8.1
20 20 * PHP slicing (phase 6). The window-tabs registry that builds on
@@ -19,9 +19,9 @@
19 19 * Extracted from `components.php` during the architecture-0.8.1
20 20 * PHP slicing (phase 6). The window-tabs registry that builds on
21 21 * top of this lives in `includes/registries/window-tabs.php`.
22 22 *
23 - * @package Desktop_Mode
23 + * @package OpenStation
24 24 */
25 25
26 26 defined( 'ABSPATH' ) || exit;
27 27
@@ -31,17 +31,17 @@
31 31 * Under the hood this:
32 32 *
33 33 * 1. Captures the $args and stores them on a module-level
34 34 * registry so the relevant admin_footer + enqueue hooks fire
35 - * only for the current user's desktop-mode shell.
35 + * only for the current user's openstation shell.
36 36 * 2. On `admin_footer` (shell-side only), emits
37 - * `<template id="desktop-mode-native-window-<id>">` wrapping the
37 + * `<template id="os-native-window-<id>">` wrapping the
38 38 * output of the `template` callback. Each registered window
39 39 * gets its own template element.
40 40 * 3. On `admin_enqueue_scripts` (shell-side), enqueues the
41 41 * caller's `script` handle if one was provided. The script
42 42 * registers a render callback at
43 - * `window.desktopModeNativeWindows[<id>]`. On every window open
43 + * `window.openStationNativeWindows[<id>]`. On every window open
44 44 * the shell clones the registered template into the body and
45 45 * then invokes the callback — render is enhancement: query
46 46 * the body for mount points your template declared, light
47 47 * them up. Without a `script` the cloned template IS the
@@ -46,12 +46,12 @@
46 46 * the body for mount points your template declared, light
47 47 * them up. Without a `script` the cloned template IS the
48 48 * window; declarative-only plugins need zero JS.
49 49 * 4. Passes a localized config blob to the script
50 - * (`desktopModeNativeWindow_<id>`) carrying the window's
50 + * (`openStationNativeWindow_<id>`) carrying the window's
51 51 * `id`, `title`, `icon`, dimensions, and `placement`. The
52 - * script then calls `wp.desktop.registerSystemTile()` +
53 - * `wp.desktop.registerWindow()` to wire up the dock tile
52 + * script then calls `wp.os.registerSystemTile()` +
53 + * `wp.os.registerWindow()` to wire up the dock tile
54 54 * and the open-on-click behaviour.
55 55 *
56 56 * Plugins write the template callback + the render callback on
57 57 * the JS side; everything else is shell plumbing. Capability gate
@@ -60,9 +60,9 @@
60 60 *
61 61 * Note on scope: the shell doesn't auto-open windows server-side
62 62 * — `registerWindow` declares availability, not presence. Users
63 63 * click the registered tile (or your plugin calls
64 - * `wp.desktop.windowManager.open()` programmatically) to surface
64 + * `wp.os.windowManager.open()` programmatically) to surface
65 65 * the window.
66 66 *
67 67 * @param string $id Doubles as window id + dock-tile id. Must
68 68 * be a kebab-case-ish slug.
@@ -72,9 +72,9 @@
72 72 * @type string $title Window + tooltip title. Required.
73 73 * @type string $icon Dashicons class or URL. Required.
74 74 * @type callable $template Echoes the window body markup.
75 75 * Wrapped on `admin_footer` in a
76 - * `<template id="desktop-mode-native-window-
76 + * `<template id="os-native-window-
77 77 * <id>">`; cloned into the window
78 78 * body on every open. The render
79 79 * callback runs against the cloned
80 80 * body, so mount points declared in
@@ -84,8 +84,61 @@
84 84 * owns the JS render callback.
85 85 * Optional — omit for a purely
86 86 * declarative window whose body is
87 87 * exactly the cloned template.
88 + * Loaded the first time the window
89 + * opens, not at boot — see
90 + * `$preload_script`.
91 + * @type string[] $scripts Companion script handles loaded
92 + * immediately before `$script`, in
93 + * the order given. For a bundle that
94 + * extends the window from outside it
95 + * — subscribing to the window's own
96 + * actions, contributing a section —
97 + * and therefore has to be in the tab
98 + * before the window's render callback
99 + * paints. Declaring it here is what
100 + * keeps it off the boot critical
101 + * path: it travels with the window
102 + * it extends. Default empty.
103 + * @type string[] $styles Companion style handles injected on
104 + * the window's first open, after the
105 + * window's own `$style`, in the order
106 + * given — so at equal specificity a
107 + * companion's overrides win, the same
108 + * source-order contract an enqueue
109 + * dependency gives. The styles-side
110 + * mirror of `$scripts`: a stylesheet
111 + * that only paints surfaces inside
112 + * this window is dead weight on every
113 + * document that never shows it —
114 + * declared here it costs nothing at
115 + * boot and never reaches chromeless
116 + * iframes at all. Unlike `$style`
117 + * (injected when the window registers,
118 + * so mid-session activations paint),
119 + * companions wait for the first open;
120 + * the deferral is the point. Default
121 + * empty.
122 + * @type bool $preload_script Load `$script` (and `$scripts`) at
123 + * shell boot instead of on first
124 + * open. Default false — a window's
125 + * bundle is dead weight until the
126 + * window opens, and the documented
127 + * contract for it is "publish a
128 + * render callback on
129 + * `window.openStationNativeWindows[
130 + * <id> ]`", which the shell reads at
131 + * open time. Opt in only when the
132 + * bundle ALSO has a boot-time job
133 + * that must run whether or not the
134 + * user ever opens the window — a
135 + * dock badge poller, a public API it
136 + * installs on `wp.os`. Prefer
137 + * splitting that job into an
138 + * always-loaded bundle over paying
139 + * the whole window's weight on every
140 + * admin page.
88 141 * @type int $width Initial width (px). Default 520.
89 142 * @type int $height Initial height (px). Default 400.
90 143 * @type int $min_width Minimum width (px). Default 280.
91 144 * @type int $min_height Minimum height (px). Default 220.
@@ -91,11 +144,51 @@
91 144 * @type int $min_height Minimum height (px). Default 220.
92 145 * @type string $placement 'dock' | 'none'. Default 'dock'.
93 146 * 'none' skips the tile (plugin
94 147 * opens the window programmatically).
148 + * A PROPOSED default only: the user's
149 + * OpenStation Preferences → Navigation
150 + * pick wins, and so does a right-click
151 + * "Keep in dock".
152 + * @type string $admin 'site' | 'network' | 'any'. Default
153 + * 'site': offered on every site's
154 + * shell and never on the network
155 + * admin's, which is right for a window
156 + * that reads the current site's REST
157 + * API. 'network' is the network
158 + * admin's shell only; 'any' is both.
159 + * @type string $nav_kind 'app' | 'control'. Default 'app'.
160 + * What the window IS, which decides
161 + * where its launcher defaults to (apps
162 + * to the desktop, controls to the
163 + * dock) and which dock zone it sits
164 + * in. Plugins want 'app'; 'control'
165 + * is for OpenStation's own
166 + * affordances.
167 + * @type int $dock_order Sort key among system tiles,
168 + * ascending; ties keep registration
169 + * order. Default 0, which places the
170 + * tile ahead of the shell's own
171 + * trailing cluster (Mio 10, Overview
172 + * 20, System 30, Exit 35, Trash 40).
173 + * Needed because registration order
174 + * is not something a plugin controls:
175 + * tiles land when their lazy script
176 + * resolves.
177 + * @type bool $placeable Whether the dock tile gets a row in
178 + * OpenStation Preferences → Apps &
179 + * Plugins, so the user can move it to
180 + * the wallpaper or hide it. Defaults
181 + * to the dock either way. Default
182 + * false, because most tiles are
183 + * load-bearing. Opt in for a window
184 + * the user can reasonably do without.
185 + * Only offer this on a window that
186 + * registers no desktop icon: the icon
187 + * already owns a row of its own.
95 188 * @type string[] $capabilities User capabilities that gate the
96 189 * registration. ANY miss returns
97 - * `WP_Error desktop_mode_capability_denied`.
190 + * `WP_Error openstation_capability_denied`.
98 191 * @type bool|string $autofocus Passed verbatim to
99 192 * `NativeWindowDef.autofocus`.
100 193 * @type string $main_tab_label Label for the "main" tab that
101 194 * displays the window's own
@@ -101,9 +194,9 @@
101 194 * displays the window's own
102 195 * `template` output. Only rendered
103 196 * when at least one additional
104 197 * tab is registered via
105 - * {@see desktop_mode_register_window_tab()}.
198 + * {@see openstation_register_window_tab()}.
106 199 * Defaults to the window's `title`.
107 200 * @type int $main_tab_padding Padding (in px) applied to the
108 201 * auto-generated tab-wrap around
109 202 * the window body. Only applies
@@ -110,15 +203,15 @@
110 203 * when additional tabs are
111 204 * registered. Default 16. Pass 0
112 205 * for edge-to-edge content.
113 206 * Filterable at runtime via
114 - * `desktop_mode_native_window_tab_wrap_padding`.
207 + * `openstation_native_window_tab_wrap_padding`.
115 208 * @type array $config Arbitrary serializable data to ship
116 209 * to the bundle alongside the script
117 210 * tag. Read in JS via
118 - * `wp.desktop.getWindowConfig( $id )`
211 + * `wp.os.getWindowConfig( $id )`
119 212 * (or directly at
120 - * `window.desktopModeWindowConfig[ $id ]`).
213 + * `window.openStationWindowConfig[ $id ]`).
121 214 * Recommended over `wp_localize_script`
122 215 * for native-window scripts because
123 216 * the lazy-load path bypasses
124 217 * `wp_print_scripts` — passing config
@@ -132,13 +225,13 @@
132 225 * @return true|WP_Error `true` on success; `WP_Error` when any
133 226 * required arg is missing/invalid or a
134 227 * declared capability is unmet.
135 228 */
136 -function desktop_mode_register_window( $id, $args = array() ) {
229 +function openstation_register_window( $id, $args = array() ) {
137 230 $id = sanitize_key( (string) $id );
138 231 if ( '' === $id ) {
139 - return desktop_mode_registration_error(
140 - 'desktop_mode_missing_id',
232 + return openstation_registration_error(
233 + 'openstation_missing_id',
141 234 __( 'Native window id is required and must be a valid slug.', 'desktop-mode' )
142 235 );
143 236 }
144 237
@@ -146,8 +239,11 @@
146 239 'title' => '',
147 240 'icon' => 'dashicons-admin-generic',
148 241 'template' => null,
149 242 'script' => '',
243 + 'scripts' => array(),
244 + 'styles' => array(),
245 + 'preload_script' => false,
150 246 // Optional WP style handle (registered with `wp_register_style()`).
151 247 // Resolved at payload-build time so the shell can lazy-inject a
152 248 // `<link rel="stylesheet">` when a peer plugin is activated
153 249 // mid-session — without this, the parent shell page already
@@ -158,27 +254,40 @@
158 254 'height' => 400,
159 255 'min_width' => 280,
160 256 'min_height' => 220,
161 257 'placement' => 'dock',
258 + 'admin' => 'site',
259 + 'nav_kind' => 'app',
260 + 'dock_order' => 0,
261 + 'placeable' => false,
162 262 'capabilities' => array(),
163 263 'autofocus' => false,
164 264 'main_tab_label' => '',
165 265 'main_tab_padding' => '',
266 + // Admin pages this window answers for, `array( id, page )` per
267 + // entry. See `App::menu()` and `openstation_apps_menu_pages()`.
268 + 'menu_pages' => array(),
166 269 'config' => array(),
167 270 );
168 - $args = wp_parse_args( $args, $defaults );
271 + $args = wp_parse_args( $args, $defaults );
272 + if ( ! in_array( $args['admin'], array( 'site', 'network', 'any' ), true ) ) {
273 + $args['admin'] = 'site';
274 + }
169 275
170 276 // Capability gate — ALL listed caps must match. Fail closed.
171 277 foreach ( (array) $args['capabilities'] as $cap ) {
172 278 if ( ! current_user_can( (string) $cap ) ) {
173 - return desktop_mode_registration_error(
174 - 'desktop_mode_capability_denied',
279 + return openstation_registration_error(
280 + 'openstation_capability_denied',
175 281 sprintf(
176 282 /* translators: %s: capability slug. */
177 283 __( 'Current user lacks the %s capability required to register this native window.', 'desktop-mode' ),
178 284 (string) $cap
179 285 ),
180 - array( 'capability' => (string) $cap, 'id' => $id )
286 + array(
287 + 'capability' => (string) $cap,
288 + 'id' => $id,
289 + )
181 290 );
182 291 }
183 292 }
184 293
@@ -183,17 +292,17 @@
183 292 }
184 293
185 294 // Required fields.
186 295 if ( '' === (string) $args['title'] ) {
187 - return desktop_mode_registration_error(
188 - 'desktop_mode_missing_title',
296 + return openstation_registration_error(
297 + 'openstation_missing_title',
189 298 __( 'Native window registration requires a non-empty `title`.', 'desktop-mode' ),
190 299 array( 'id' => $id )
191 300 );
192 301 }
193 302 if ( ! is_callable( $args['template'] ) ) {
194 - return desktop_mode_registration_error(
195 - 'desktop_mode_invalid_template',
303 + return openstation_registration_error(
304 + 'openstation_invalid_template',
196 305 __( 'Native window registration requires a callable `template` that echoes the template body.', 'desktop-mode' ),
197 306 array( 'id' => $id )
198 307 );
199 308 }
@@ -201,8 +310,17 @@
201 310 $placement = in_array( $args['placement'], array( 'dock', 'none' ), true )
202 311 ? $args['placement']
203 312 : 'dock';
204 313
314 + // What the window IS, which is what decides where its launcher
315 + // goes by default and which dock zone it sits in. `'app'` for an
316 + // installed app (the default, and what every plugin wants);
317 + // `'control'` for an OpenStation affordance — the Trash is the
318 + // only shipped one.
319 + $nav_kind = in_array( $args['nav_kind'], array( 'app', 'control' ), true )
320 + ? $args['nav_kind']
321 + : 'app';
322 +
205 323 $entry = array(
206 324 'id' => $id,
207 325 'title' => (string) $args['title'],
208 326 'icon' => (string) $args['icon'],
@@ -207,8 +325,32 @@
207 325 'title' => (string) $args['title'],
208 326 'icon' => (string) $args['icon'],
209 327 'template' => $args['template'],
210 328 'script' => (string) $args['script'],
329 + // Companion handles, deduped and stripped of empties so the
330 + // payload builder can resolve the list without re-checking.
331 + 'scripts' => array_values(
332 + array_unique(
333 + array_filter(
334 + array_map( 'strval', (array) $args['scripts'] ),
335 + static function ( $handle ) {
336 + return '' !== $handle;
337 + }
338 + )
339 + )
340 + ),
341 + // Companion style handles, same dedupe/strip as `scripts`.
342 + 'styles' => array_values(
343 + array_unique(
344 + array_filter(
345 + array_map( 'strval', (array) $args['styles'] ),
346 + static function ( $handle ) {
347 + return '' !== $handle;
348 + }
349 + )
350 + )
351 + ),
352 + 'preload_script' => (bool) $args['preload_script'],
211 353 'style' => (string) $args['style'],
212 354 'width' => (int) $args['width'],
213 355 'height' => (int) $args['height'],
214 356 'min_width' => (int) $args['min_width'],
@@ -213,20 +355,33 @@
213 355 'height' => (int) $args['height'],
214 356 'min_width' => (int) $args['min_width'],
215 357 'min_height' => (int) $args['min_height'],
216 358 'placement' => $placement,
359 + 'nav_kind' => $nav_kind,
360 + // Which admin's shell offers it; see the `admin` arg.
361 + 'admin' => $args['admin'],
362 + // Sort key among system tiles, ascending. `0` (the default)
363 + // puts a plugin's tile ahead of the shell's own trailing
364 + // cluster — Mio 10, Overview 20, System 30 — which is where a
365 + // launcher belongs. Trash uses 40 to sit at the very end.
366 + 'dock_order' => (int) $args['dock_order'],
367 + 'placeable' => (bool) $args['placeable'],
217 368 'autofocus' => $args['autofocus'],
218 369 'main_tab_label' => (string) $args['main_tab_label'],
219 - // Stored as-is (string or int). `desktop_mode_build_native_window_template_html`
370 + // Stored as-is (string or int). `openstation_build_native_window_template_html`
220 371 // coerces to int and falls back to 16 when absent.
221 372 'main_tab_padding' => $args['main_tab_padding'],
222 373 // Bundle-bound config delivered through the same path as
223 374 // `wp_localize_script` `extra['data']` — see the `config` doc
224 - // in this function's `$args` block and `desktop_mode_resolve_script_payload()`
375 + // in this function's `$args` block and `openstation_resolve_script_payload()`
225 376 // for how it lands on the wire.
377 + // Admin pages this window answers for, `array( id, page )` per
378 + // entry — dropped here once, which is why the shell saw an
379 + // empty list and claimed none of them.
380 + 'menu_pages' => is_array( $args['menu_pages'] ) ? array_values( $args['menu_pages'] ) : array(),
226 381 'config' => is_array( $args['config'] ) ? $args['config'] : array(),
227 382 );
228 - desktop_mode_native_window_registry( $id, $entry );
383 + openstation_native_window_registry( $id, $entry );
229 384
230 385 /**
231 386 * Fires after a native desktop window is successfully registered.
232 387 *
@@ -232,9 +387,9 @@
232 387 *
233 388 * Lets plugins react to registrations made by other plugins —
234 389 * e.g. a widget that auto-opens when a given window registers,
235 390 * or analytics tracking of which windows the current install
236 - * exposes. Does NOT fire when `desktop_mode_register_window()`
391 + * exposes. Does NOT fire when `openstation_register_window()`
237 392 * returns a `WP_Error`.
238 393 *
239 394 * @param string $id The window id.
240 395 * @param array $entry The stored registry entry (id, title,
@@ -240,9 +395,9 @@
240 395 * @param array $entry The stored registry entry (id, title,
241 396 * icon, template callback, script handle,
242 397 * size defaults, placement, autofocus).
243 398 */
244 - do_action( 'desktop_mode_native_window_registered', $id, $entry );
399 + do_action( 'openstation_native_window_registered', $id, $entry );
245 400
246 401 return true;
247 402 }
248 403
@@ -247,9 +402,9 @@
247 402 }
248 403
249 404 /**
250 405 * Internal module-level registry for native windows registered
251 - * via {@see desktop_mode_register_window()}. Passing a second
406 + * via {@see openstation_register_window()}. Passing a second
252 407 * argument stores the entry; passing only the id returns the
253 408 * stored value (or null). Kept small and side-effect-free so
254 409 * tests can introspect.
255 410 *
@@ -259,9 +414,9 @@
259 414 * @param array|null $entry Entry to store, or null to just read.
260 415 * @return array|null Either the stored entry or the full registry
261 416 * (when id is empty).
262 417 */
263 -function desktop_mode_native_window_registry( $id = '', $entry = null ) {
418 +function openstation_native_window_registry( $id = '', $entry = null ) {
264 419 static $store = array();
265 420
266 421 if ( '' === (string) $id ) {
267 422 return $store;
@@ -280,16 +435,16 @@
280 435 *
281 436 * Templates are inert until JS clones them out of the `<template>`
282 437 * tag — but Plugin Check still requires escape-on-output. The list
283 438 * extends `wp_kses_allowed_html( 'post' )` with form controls,
284 - * `<wpd-*>` web components, and dashicon spans, plus permissive
285 - * `data-*`, `aria-*`, and component-specific attributes. Plugins
439 + * `<os-*>` web components, and dashicon spans, plus permissive
440 + * `data-*`, common ARIA, and component-specific attributes. Plugins
286 441 * registering their own native windows can extend the list via the
287 - * `desktop_mode_native_window_allowed_html` filter below.
442 + * `openstation_native_window_allowed_html` filter below.
288 443 *
289 444 * @return array<string,array<string,bool>>
290 445 */
291 -function desktop_mode_native_window_allowed_html() {
446 +function openstation_native_window_allowed_html() {
292 447 $base = wp_kses_allowed_html( 'post' );
293 448
294 449 $global_attrs = array(
295 450 'id' => true,
@@ -305,11 +460,17 @@
305 460 'dir' => true,
306 461 'draggable' => true,
307 462 'contenteditable' => true,
308 463 'data-*' => true,
309 - 'aria-*' => true,
464 + // `wp_kses` only treats the `data-*` wildcard specially. ARIA
465 + // attributes must be admitted by their exact names or they are
466 + // silently stripped from native-window templates.
467 + 'aria-label' => true,
468 + 'aria-labelledby' => true,
469 + 'aria-current' => true,
470 + 'aria-hidden' => true,
310 471 // `full-width` is a layout-level flag honoured by
311 - // `<wpd-form>` (and any future wpd-* container that opts in
472 + // `<os-form>` (and any future os-* container that opts in
312 473 // to row-spanning slotted children). Lives in the global
313 474 // allowlist so a plain `<div full-width>` wrapper isn't
314 475 // stripped by kses on its way through the template.
315 476 'full-width' => true,
@@ -319,38 +480,38 @@
319 480 $global_attrs,
320 481 array(
321 482 'name' => true,
322 483 'value' => true,
323 - 'placeholder' => true,
324 - 'required' => true,
325 - 'disabled' => true,
326 - 'readonly' => true,
327 - 'checked' => true,
328 - 'selected' => true,
329 - 'min' => true,
330 - 'max' => true,
331 - 'step' => true,
332 - 'minlength' => true,
333 - 'maxlength' => true,
334 - 'pattern' => true,
484 + 'placeholder' => true,
485 + 'required' => true,
486 + 'disabled' => true,
487 + 'readonly' => true,
488 + 'checked' => true,
489 + 'selected' => true,
490 + 'min' => true,
491 + 'max' => true,
492 + 'step' => true,
493 + 'minlength' => true,
494 + 'maxlength' => true,
495 + 'pattern' => true,
335 496 'autocomplete' => true,
336 - 'autofocus' => true,
337 - 'multiple' => true,
338 - 'rows' => true,
339 - 'cols' => true,
340 - 'wrap' => true,
341 - 'size' => true,
342 - 'for' => true,
343 - 'form' => true,
344 - 'type' => true,
345 - 'accept' => true,
346 - 'list' => true,
347 - 'src' => true,
348 - 'href' => true,
349 - 'target' => true,
350 - 'rel' => true,
351 - 'open' => true,
352 - 'variant' => true,
497 + 'autofocus' => true,
498 + 'multiple' => true,
499 + 'rows' => true,
500 + 'cols' => true,
501 + 'wrap' => true,
502 + 'size' => true,
503 + 'for' => true,
504 + 'form' => true,
505 + 'type' => true,
506 + 'accept' => true,
507 + 'list' => true,
508 + 'src' => true,
509 + 'href' => true,
510 + 'target' => true,
511 + 'rel' => true,
512 + 'open' => true,
513 + 'variant' => true,
353 514 )
354 515 );
355 516
356 517 $wpd_attrs = array_merge(
@@ -355,115 +516,218 @@
355 516
356 517 $wpd_attrs = array_merge(
357 518 $form_attrs,
358 519 array(
359 - 'gap' => true,
360 - 'padding' => true,
361 - 'align' => true,
362 - 'justify' => true,
363 - 'direction' => true,
364 - 'wrap' => true,
365 - 'inset' => true,
366 - 'icon' => true,
367 - 'tone' => true,
368 - 'size' => true,
369 - 'shape' => true,
370 - 'badge' => true,
371 - 'selectable' => true,
372 - 'sticky-header' => true,
520 + 'gap' => true,
521 + 'padding' => true,
522 + 'align' => true,
523 + 'justify' => true,
524 + 'direction' => true,
525 + 'wrap' => true,
526 + 'inset' => true,
527 + 'icon' => true,
528 + 'tone' => true,
529 + 'size' => true,
530 + 'shape' => true,
531 + 'badge' => true,
532 + 'selectable' => true,
533 + 'sticky-header' => true,
373 534 'sticky-columns' => true,
374 - 'hover' => true,
375 - 'striped' => true,
376 - 'bordered' => true,
377 - 'compact' => true,
378 - 'loading' => true,
379 - 'loading-rows' => true,
380 - 'columns' => true,
381 - 'rows' => true,
382 - 'sortable' => true,
383 - 'expandable' => true,
384 - 'preset' => true,
385 - 'label' => true,
386 - 'description' => true,
387 - 'orientation' => true,
388 - 'level' => true,
389 - 'collapsed' => true,
390 - // `<wpd-form>` props + the `full-width` row span flag
535 + 'hover' => true,
536 + 'striped' => true,
537 + 'bordered' => true,
538 + 'compact' => true,
539 + 'loading' => true,
540 + 'loading-rows' => true,
541 + 'empty' => true,
542 + 'columns' => true,
543 + 'rows' => true,
544 + 'sortable' => true,
545 + 'expandable' => true,
546 + 'preset' => true,
547 + 'label' => true,
548 + 'heading' => true,
549 + 'description' => true,
550 + 'orientation' => true,
551 + 'level' => true,
552 + 'collapsed' => true,
553 + // `<os-form>` props + the `full-width` row span flag
391 554 // honoured by the form's slotted-child layout rule.
392 - 'submit-label' => true,
393 - 'reset-label' => true,
394 - 'busy' => true,
395 - 'error' => true,
396 - 'min-column' => true,
397 - 'show-reset' => true,
398 - 'reveal' => true,
399 - 'full-width' => true,
555 + 'submit-label' => true,
556 + 'reset-label' => true,
557 + 'busy' => true,
558 + 'error' => true,
559 + 'min-column' => true,
560 + 'show-reset' => true,
561 + 'reveal' => true,
562 + 'full-width' => true,
400 563 )
401 564 );
402 565
403 566 // Built-in HTML elements the templates rely on.
404 567 $extra = array(
405 - 'form' => $form_attrs,
406 - 'fieldset' => $form_attrs,
407 - 'legend' => $global_attrs,
408 - 'label' => $form_attrs,
409 - 'input' => $form_attrs,
410 - 'select' => $form_attrs,
411 - 'option' => $form_attrs,
412 - 'optgroup' => $form_attrs,
413 - 'textarea' => $form_attrs,
414 - 'button' => $form_attrs,
415 - 'output' => $form_attrs,
416 - 'datalist' => $global_attrs,
417 - 'progress' => $form_attrs,
418 - 'meter' => $form_attrs,
419 - 'details' => $global_attrs,
420 - 'summary' => $global_attrs,
421 - 'dialog' => $global_attrs,
422 - 'header' => $global_attrs,
423 - 'footer' => $global_attrs,
424 - 'main' => $global_attrs,
425 - 'nav' => $global_attrs,
426 - 'section' => $global_attrs,
427 - 'article' => $global_attrs,
428 - 'aside' => $global_attrs,
429 - 'figure' => $global_attrs,
568 + 'form' => $form_attrs,
569 + 'fieldset' => $form_attrs,
570 + 'legend' => $global_attrs,
571 + 'label' => $form_attrs,
572 + 'input' => $form_attrs,
573 + 'select' => $form_attrs,
574 + 'option' => $form_attrs,
575 + 'optgroup' => $form_attrs,
576 + 'textarea' => $form_attrs,
577 + 'button' => $form_attrs,
578 + 'output' => $form_attrs,
579 + 'datalist' => $global_attrs,
580 + 'progress' => $form_attrs,
581 + 'meter' => $form_attrs,
582 + 'details' => $global_attrs,
583 + 'summary' => $global_attrs,
584 + 'dialog' => $global_attrs,
585 + 'header' => $global_attrs,
586 + 'footer' => $global_attrs,
587 + 'main' => $global_attrs,
588 + 'nav' => $global_attrs,
589 + 'section' => $global_attrs,
590 + 'article' => $global_attrs,
591 + 'aside' => $global_attrs,
592 + 'figure' => $global_attrs,
430 593 'figcaption' => $global_attrs,
431 - 'time' => array_merge( $global_attrs, array( 'datetime' => true ) ),
432 - 'mark' => $global_attrs,
433 - 'small' => $global_attrs,
434 - 'svg' => array_merge( $global_attrs, array( 'viewbox' => true, 'width' => true, 'height' => true, 'fill' => true, 'stroke' => true, 'xmlns' => true ) ),
435 - 'path' => array( 'd' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'class' => true ),
436 - 'g' => array( 'class' => true, 'transform' => true, 'fill' => true ),
437 - 'circle' => array( 'cx' => true, 'cy' => true, 'r' => true, 'fill' => true, 'stroke' => true, 'class' => true ),
438 - 'rect' => array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'ry' => true, 'fill' => true, 'stroke' => true, 'class' => true ),
439 - 'line' => array( 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, 'stroke' => true, 'stroke-width' => true, 'class' => true ),
440 - 'polyline' => array( 'points' => true, 'fill' => true, 'stroke' => true, 'class' => true ),
441 - 'polygon' => array( 'points' => true, 'fill' => true, 'stroke' => true, 'class' => true ),
442 - 'use' => array( 'href' => true, 'class' => true ),
594 + 'time' => array_merge( $global_attrs, array( 'datetime' => true ) ),
595 + 'mark' => $global_attrs,
596 + 'small' => $global_attrs,
597 + 'svg' => array_merge(
598 + $global_attrs,
599 + array(
600 + 'viewbox' => true,
601 + 'width' => true,
602 + 'height' => true,
603 + 'fill' => true,
604 + 'stroke' => true,
605 + 'xmlns' => true,
606 + )
607 + ),
608 + 'path' => array(
609 + 'd' => true,
610 + 'fill' => true,
611 + 'stroke' => true,
612 + 'stroke-width' => true,
613 + 'stroke-linecap' => true,
614 + 'stroke-linejoin' => true,
615 + 'class' => true,
616 + ),
617 + 'g' => array(
618 + 'class' => true,
619 + 'transform' => true,
620 + 'fill' => true,
621 + ),
622 + 'circle' => array(
623 + 'cx' => true,
624 + 'cy' => true,
625 + 'r' => true,
626 + 'fill' => true,
627 + 'stroke' => true,
628 + 'class' => true,
629 + ),
630 + 'rect' => array(
631 + 'x' => true,
632 + 'y' => true,
633 + 'width' => true,
634 + 'height' => true,
635 + 'rx' => true,
636 + 'ry' => true,
637 + 'fill' => true,
638 + 'stroke' => true,
639 + 'class' => true,
640 + ),
641 + 'line' => array(
642 + 'x1' => true,
643 + 'y1' => true,
644 + 'x2' => true,
645 + 'y2' => true,
646 + 'stroke' => true,
647 + 'stroke-width' => true,
648 + 'class' => true,
649 + ),
650 + 'polyline' => array(
651 + 'points' => true,
652 + 'fill' => true,
653 + 'stroke' => true,
654 + 'class' => true,
655 + ),
656 + 'polygon' => array(
657 + 'points' => true,
658 + 'fill' => true,
659 + 'stroke' => true,
660 + 'class' => true,
661 + ),
662 + 'use' => array(
663 + 'href' => true,
664 + 'class' => true,
665 + ),
443 666 );
444 667
445 - // `<wpd-*>` web components — every shipped tag plus a permissive
668 + // `<os-*>` web components — every shipped tag plus a permissive
446 669 // open door for new ones added by plugin templates.
447 670 $wpd_tags = array(
448 - 'wpd-stack', 'wpd-cluster', 'wpd-grid', 'wpd-spacer', 'wpd-divider',
449 - 'wpd-tabs', 'wpd-tab', 'wpd-tabpanel',
450 - 'wpd-segmented', 'wpd-segment',
451 - 'wpd-button', 'wpd-icon-button', 'wpd-button-group',
452 - 'wpd-text-field', 'wpd-textarea', 'wpd-search-field',
453 - 'wpd-select', 'wpd-option', 'wpd-checkbox', 'wpd-checkbox-label',
454 - 'wpd-radio', 'wpd-radio-group', 'wpd-form',
455 - 'wpd-switch', 'wpd-slider',
456 - 'wpd-table', 'wpd-table-column', 'wpd-table-row', 'wpd-table-cell',
457 - 'wpd-card', 'wpd-list', 'wpd-list-item',
458 - 'wpd-badge', 'wpd-pill', 'wpd-tag', 'wpd-chip',
459 - 'wpd-spinner', 'wpd-skeleton', 'wpd-empty-state',
460 - 'wpd-tooltip', 'wpd-popover', 'wpd-menu', 'wpd-menu-item',
461 - 'wpd-modal', 'wpd-drawer', 'wpd-toast',
462 - 'wpd-icon', 'wpd-avatar', 'wpd-heading', 'wpd-text', 'wpd-link',
463 - 'wpd-banner', 'wpd-alert', 'wpd-callout',
464 - 'wpd-form-row', 'wpd-form-section', 'wpd-help-text',
465 - 'wpd-toolbar', 'wpd-toolbar-group',
671 + 'os-stack',
672 + 'os-cluster',
673 + 'os-grid',
674 + 'os-spacer',
675 + 'os-divider',
676 + 'os-tabs',
677 + 'os-tab',
678 + 'os-tabpanel',
679 + 'os-segmented',
680 + 'os-segment',
681 + 'os-button',
682 + 'os-icon-button',
683 + 'os-button-group',
684 + 'os-text-field',
685 + 'os-textarea',
686 + 'os-search-field',
687 + 'os-select',
688 + 'os-option',
689 + 'os-checkbox',
690 + 'os-checkbox-label',
691 + 'os-radio',
692 + 'os-radio-group',
693 + 'os-form',
694 + 'os-switch',
695 + 'os-slider',
696 + 'os-table',
697 + 'os-table-column',
698 + 'os-table-row',
699 + 'os-table-cell',
700 + 'os-card',
701 + 'os-list',
702 + 'os-list-item',
703 + 'os-badge',
704 + 'os-pill',
705 + 'os-tag',
706 + 'os-chip',
707 + 'os-spinner',
708 + 'os-skeleton',
709 + 'os-empty-state',
710 + 'os-tooltip',
711 + 'os-popover',
712 + 'os-menu',
713 + 'os-menu-item',
714 + 'os-modal',
715 + 'os-drawer',
716 + 'os-toast',
717 + 'os-icon',
718 + 'os-avatar',
719 + 'os-heading',
720 + 'os-text',
721 + 'os-link',
722 + 'os-banner',
723 + 'os-alert',
724 + 'os-callout',
725 + 'os-form-row',
726 + 'os-form-section',
727 + 'os-help-text',
728 + 'os-toolbar',
729 + 'os-toolbar-group',
466 730 );
467 731 foreach ( $wpd_tags as $tag ) {
468 732 $extra[ $tag ] = $wpd_attrs;
469 733 }
@@ -470,9 +734,9 @@
470 734
471 735 $allowed = array_merge( $base, $extra );
472 736
473 737 // Promote the framework's global attrs (`slot`, `part`,
474 - // `full-width`, `data-*`, `aria-*`, …) to EVERY allowed tag —
738 + // `full-width`, `data-*`, common ARIA, …) to EVERY allowed tag —
475 739 // otherwise plain wrappers like `<div slot="header">` lose
476 740 // their `slot` attribute on the way through kses and get
477 741 // projected into the default slot instead of the named one.
478 742 // Caught by inspection when the Add User form's header
@@ -496,17 +760,17 @@
496 760 * markup not covered here.
497 761 *
498 762 * @param array $allowed wp_kses-shaped allowlist.
499 763 */
500 - return (array) apply_filters( 'desktop_mode_native_window_allowed_html', $allowed );
764 + return (array) apply_filters( 'openstation_native_window_allowed_html', $allowed );
501 765 }
502 766
503 767 /**
504 768 * Run `wp_kses` on a native-window template body with the framework
505 - * allowlist, **auto-extending the allowlist with every `<wpd-*>` tag
769 + * allowlist, **auto-extending the allowlist with every `<os-*>` tag
506 770 * the template actually uses.**
507 771 *
508 - * The pain this fixes: each shipped `<wpd-*>` component had to be
772 + * The pain this fixes: each shipped `<os-*>` component had to be
509 773 * manually added to the `$wpd_tags` list above, and the failure mode
510 774 * of forgetting it was silent — kses would strip the tag, the
511 775 * template would render as a sea of unparented children, and you'd
512 776 * spend an afternoon working out why "the form has no buttons."
@@ -511,27 +775,27 @@
511 775 * template would render as a sea of unparented children, and you'd
512 776 * spend an afternoon working out why "the form has no buttons."
513 777 *
514 778 * Plugin authors registering a new component now only need to
515 - * `defineComponent('wpd-foo', WpdFoo)` on the JS side and use
516 - * `<wpd-foo>` in their template — this helper finds the tag at
779 + * `defineComponent('os-foo', OsFoo)` on the JS side and use
780 + * `<os-foo>` in their template — this helper finds the tag at
517 781 * render time, tags it onto the allowlist with the standard
518 782 * permissive attrs, and runs kses with the extended list.
519 783 *
520 784 * Every callsite in the framework that previously did the
521 - * `wp_kses( $html, desktop_mode_native_window_allowed_html() )`
785 + * `wp_kses( $html, openstation_native_window_allowed_html() )`
522 786 * dance can call this instead and get tag-discovery for free.
523 787 *
524 788 * @param string $html Template HTML to sanitize.
525 789 * @return string Sanitized HTML.
526 790 */
527 -function desktop_mode_kses_native_window_template( $html ) {
528 - $allowed = desktop_mode_native_window_allowed_html();
791 +function openstation_kses_native_window_template( $html ) {
792 + $allowed = openstation_native_window_allowed_html();
529 793
530 - if ( preg_match_all( '/<(wpd-[a-z][a-z0-9-]*)\b/i', (string) $html, $matches ) ) {
531 - $unique = array_unique( array_map( 'strtolower', $matches[1] ) );
532 - $wpd_attrs = isset( $allowed['wpd-button'] )
533 - ? $allowed['wpd-button']
794 + if ( preg_match_all( '/<(os-[a-z][a-z0-9-]*)\b/i', (string) $html, $matches ) ) {
795 + $unique = array_unique( array_map( 'strtolower', $matches[1] ) );
796 + $wpd_attrs = isset( $allowed['os-button'] )
797 + ? $allowed['os-button']
534 798 : array();
535 799 foreach ( $unique as $tag ) {
536 800 if ( ! isset( $allowed[ $tag ] ) ) {
537 801 $allowed[ $tag ] = $wpd_attrs;
@@ -544,11 +808,11 @@
544 808
545 809 /**
546 810 * Render a native window's template HTML to a string, wrapping
547 811 * with tabs when the window has at least one additional tab
548 - * registered. Shared by `desktop_mode_render_native_window_templates()`
812 + * registered. Shared by `openstation_render_native_window_templates()`
549 813 * (which emits the live `<template>` element) and
550 - * `desktop_mode_build_native_windows_payload()` (which captures the same
814 + * `openstation_build_native_windows_payload()` (which captures the same
551 815 * string for the shell config so mid-session activation can inject
552 816 * the template without a reload).
553 817 *
554 818 * Single-tab windows (no additional tabs registered) render the
@@ -557,14 +821,14 @@
557 821 *
558 822 * @param array $entry Window registry entry.
559 823 * @return string Template body HTML (no outer `<template>` tag).
560 824 */
561 -function desktop_mode_build_native_window_template_html( $entry ) {
825 +function openstation_build_native_window_template_html( $entry ) {
562 826 if ( ! is_array( $entry ) || ! is_callable( $entry['template'] ) ) {
563 827 return '';
564 828 }
565 829
566 - $tabs = desktop_mode_get_native_window_tabs( $entry['id'] );
830 + $tabs = openstation_get_native_window_tabs( $entry['id'] );
567 831 $has_extras = count( $tabs ) > 1;
568 832
569 833 // Fast path — single-pane window, no wrapping.
570 834 if ( ! $has_extras ) {
@@ -572,21 +836,30 @@
572 836 call_user_func( $entry['template'] );
573 837 return (string) ob_get_clean();
574 838 }
575 839
576 - // Multi-tab window — wrap in <wpd-stack> + <wpd-tabs> + one
577 - // <wpd-tabpanel> per tab. The default active tab is the main
578 - // one (the window's own template). Plugin authors still get to
579 - // declare their own tab-change side effects via the
580 - // `wpd-tab-change` event bubbled by <wpd-tabs>.
840 + // Multi-tab window — wrap in <os-stack> + one <os-tabpanel> per
841 + // tab. The default active tab is the main one (the window's own
842 + // template).
581 843 //
844 + // The tab STRIP is deliberately absent from this markup. It is
845 + // built by the shell in the window chrome, under the title bar,
846 + // from the same tab metadata this function walks (the payload
847 + // carries it as `tabs`). One tab strip per window, in one place,
848 + // whether the window is an admin page in an iframe or a native
849 + // window like this one.
850 + //
851 + // Plugin authors declare tab-change side effects by listening for
852 + // `os-window-tab-change` on the window element; see
853 + // docs/migration-window-tabs.md.
854 + //
582 855 // The wrap's padding is plugin-controllable two ways:
583 - // 1. `main_tab_padding` arg on `desktop_mode_register_window` —
584 - // a per-window override. `0` opts into edge-to-edge
585 - // content.
586 - // 2. `desktop_mode_native_window_tab_wrap_padding` filter for
587 - // late-bound overrides (e.g. a theme that wants every
588 - // tabbed window to adopt a narrower inset).
856 + // 1. `main_tab_padding` arg on `openstation_register_window` —
857 + // a per-window override. `0` opts into edge-to-edge
858 + // content.
859 + // 2. `openstation_native_window_tab_wrap_padding` filter for
860 + // late-bound overrides (e.g. a theme that wants every
861 + // tabbed window to adopt a narrower inset).
589 862 // Default stays 16px so existing plugins don't shift.
590 863 $default_padding = isset( $entry['main_tab_padding'] )
591 864 && '' !== (string) $entry['main_tab_padding']
592 865 ? (int) $entry['main_tab_padding']
@@ -593,9 +866,9 @@
593 866 : 16;
594 867 /**
595 868 * Filters the padding (in px) applied to the auto-generated
596 869 * tab wrap around a native window's template body. The shell
597 - * emits the wrap as `<wpd-stack padding="N">`; the CSS-as-
870 + * emits the wrap as `<os-stack padding="N">`; the CSS-as-
598 871 * attribute pipeline at the client translates that to
599 872 * `style.padding`.
600 873 *
601 874 * Return `0` for edge-to-edge content. Negative values are
@@ -604,9 +877,9 @@
604 877 * @param int $padding Default padding in px.
605 878 * @param string $window_id The native window id.
606 879 */
607 880 $padding = (int) apply_filters(
608 - 'desktop_mode_native_window_tab_wrap_padding',
881 + 'openstation_native_window_tab_wrap_padding',
609 882 $default_padding,
610 883 (string) $entry['id']
611 884 );
612 885 if ( $padding < 0 ) {
@@ -612,36 +885,25 @@
612 885 if ( $padding < 0 ) {
613 886 $padding = 0;
614 887 }
615 888
616 - $buffer = sprintf(
617 - '<wpd-stack gap="12" padding="%d">',
889 + $buffer = sprintf(
890 + '<os-stack gap="12" padding="%d">',
618 891 $padding
619 892 );
620 - $buffer .= '<wpd-tabs value="' . esc_attr( DESKTOP_MODE_NATIVE_WINDOW_MAIN_TAB ) . '">';
621 - foreach ( $tabs as $tab ) {
622 - $buffer .= sprintf(
623 - '<wpd-tab value="%s">%s</wpd-tab>',
624 - esc_attr( $tab['value'] ),
625 - esc_html( $tab['label'] )
626 - );
627 - }
628 - $buffer .= '</wpd-tabs>';
629 893
630 894 // Stamp `hidden` on every non-active panel directly in the
631 - // emitted HTML. The client-side `<wpd-tabs>` syncs panel
632 - // visibility on `value` changes, but its initial sync runs
633 - // inside a microtask — and panel siblings may not have upgraded
634 - // in time on first paint. Setting the attribute server-side
635 - // makes first paint correct regardless of upgrade order; the JS
636 - // keeps owning subsequent transitions.
895 + // emitted HTML. The shell takes over panel visibility as soon as
896 + // it declares the strip, but that happens after the template is
897 + // in the body — setting the attribute server-side makes first
898 + // paint correct rather than flashing every pane at once.
637 899 foreach ( $tabs as $tab ) {
638 900 if ( ! is_callable( $tab['template'] ) ) {
639 901 continue;
640 902 }
641 - $is_active = DESKTOP_MODE_NATIVE_WINDOW_MAIN_TAB === $tab['value'];
903 + $is_active = OPENSTATION_NATIVE_WINDOW_MAIN_TAB === $tab['value'];
642 904 $buffer .= sprintf(
643 - '<wpd-tabpanel for="%s"%s>',
905 + '<os-tabpanel for="%s"%s>',
644 906 esc_attr( $tab['value'] ),
645 907 $is_active ? '' : ' hidden'
646 908 );
647 909 ob_start();
@@ -646,35 +908,99 @@
646 908 );
647 909 ob_start();
648 910 call_user_func( $tab['template'] );
649 911 $buffer .= (string) ob_get_clean();
650 - $buffer .= '</wpd-tabpanel>';
912 + $buffer .= '</os-tabpanel>';
651 913 }
652 914
653 - $buffer .= '</wpd-stack>';
915 + $buffer .= '</os-stack>';
654 916 return $buffer;
655 917 }
656 918
657 919 /**
658 - * Enqueue every registered native window's script when the shell
659 - * is active. Runs on `admin_enqueue_scripts` alongside the main
660 - * shell enqueue so ordering (shell → plugin scripts) is
661 - * deterministic.
920 + * Run a native window's registered `config` through the
921 + * `openstation_native_window_config` filter, normalized to an array.
922 + *
923 + * Called at BOTH serialization points — the eager inline-script
924 + * attach in `openstation_enqueue_native_window_scripts()` and the
925 + * lazy `scriptL10n` synthesis in
926 + * `openstation_build_native_windows_payload()` — so the filter sees
927 + * every copy of the blob that can reach a browser.
928 + *
929 + * @param array $entry Registry entry (needs `id`; `config` optional).
930 + * @return array Filtered config. Empty array when nothing to ship.
662 931 */
663 -function desktop_mode_enqueue_native_window_scripts() {
664 - if ( ! desktop_mode_is_enabled() || desktop_mode_is_chromeless_request() || desktop_mode_is_classic_request() ) {
932 +function openstation_filter_native_window_config( $entry ) {
933 + $config = isset( $entry['config'] ) && is_array( $entry['config'] )
934 + ? $entry['config']
935 + : array();
936 +
937 + /**
938 + * Filter a native window's config blob at emit time.
939 + *
940 + * The registry snapshots `config` when `openstation_register_window()`
941 + * runs — usually `init`. This filter runs when the blob is
942 + * serialized for the browser (enqueue time on the eager path,
943 + * payload-build time on the lazy path), so values that depend on
944 + * hooks registered later in the bootstrap can be refreshed without
945 + * moving the whole registration. The WP Explorer uses it to
946 + * re-collect `previewActions` so plugins may add
947 + * `openstation_my_wordpress_preview_actions` callbacks any time
948 + * during a normal bootstrap, not just before `init` 99.
949 + *
950 + * Runs per request, after the current user is determined —
951 + * capability-gated values are safe to compute here.
952 + *
953 + * **Status: Experimental**
954 + *
955 + * @param array $config Config blob as registered (empty array
956 + * when the window registered none).
957 + * @param string $window_id Native window id.
958 + */
959 + $config = apply_filters( 'openstation_native_window_config', $config, (string) $entry['id'] );
960 +
961 + return is_array( $config ) ? $config : array();
962 +}
963 +
964 +/**
965 + * Attach every registered native window's script data, and enqueue
966 + * the handful of bundles that asked to load at boot.
967 + *
968 + * **A native window's bundle is not enqueued here.** It loads the
969 + * first time the window opens: the shell reads the render callback
970 + * off `window.openStationNativeWindows[ <id> ]` at open time, so a
971 + * bundle printed at boot is weight on every admin page the window is
972 + * never opened from — and between WP Explorer, Posts, Plugins,
973 + * Comments, the Recycle Bin, Content Graph, Games and the agent
974 + * runner that came to well over a megabyte before a single window
975 + * had been clicked. `preload_script` is the opt-out for a bundle
976 + * with a genuine boot-time job.
977 + *
978 + * What still happens for EVERY window is the data attach: the
979 + * localize blob and the `config` inline. Those hang off the
980 + * REGISTERED handle whether or not it is enqueued, which is exactly
981 + * how the lazy path gets them — `openstation_resolve_script_payload()`
982 + * harvests both into the payload for the shell to replay around the
983 + * script tag it injects. Hence priority 5: `openstation_enqueue_assets()`
984 + * builds that payload at 10, and data attached after it would ship a
985 + * bundle with no config.
986 + */
987 +function openstation_enqueue_native_window_scripts() {
988 + if ( ! openstation_is_shell_request() ) {
665 989 return;
666 990 }
667 - $registry = desktop_mode_native_window_registry();
991 + $registry = openstation_native_window_registry();
668 992 if ( ! is_array( $registry ) ) {
669 993 return;
670 994 }
671 995 foreach ( $registry as $entry ) {
672 - // Enqueue per-tab scripts — each tab registration can carry
673 - // its own script handle so a tab's JS module stays scoped to
674 - // that tab. Main tab uses the window's own `script`; it's
675 - // enqueued below alongside the localize call.
676 - $tabs = desktop_mode_get_native_window_tabs( $entry['id'] );
996 + $preload = ! empty( $entry['preload_script'] );
997 +
998 + // Per-tab scripts stay eager. The shell has no lazy path for
999 + // them — a tab's script is not part of the window's own
1000 + // bundle chain — so deferring here would simply break the
1001 + // tab. The main tab uses the window's own `script`.
1002 + $tabs = openstation_get_native_window_tabs( $entry['id'] );
677 1003 foreach ( $tabs as $tab ) {
678 1004 if ( $tab['is_main'] || empty( $tab['script'] ) ) {
679 1005 continue;
680 1006 }
@@ -683,25 +1009,38 @@
683 1009
684 1010 if ( empty( $entry['script'] ) ) {
685 1011 continue;
686 1012 }
687 - wp_enqueue_script( $entry['script'] );
1013 + if ( $preload ) {
1014 + wp_enqueue_script( $entry['script'] );
1015 + foreach ( (array) $entry['scripts'] as $companion ) {
1016 + wp_enqueue_script( $companion );
1017 + }
1018 + // Preload means "everything at boot" — companion styles
1019 + // ride along so the window paints styled on a preloaded
1020 + // first open, same as its scripts are already parsed.
1021 + if ( ! empty( $entry['styles'] ) ) {
1022 + foreach ( (array) $entry['styles'] as $companion_style ) {
1023 + wp_enqueue_style( $companion_style );
1024 + }
1025 + }
1026 + }
688 1027 // Localize the config the JS side reads to register itself.
689 1028 wp_localize_script(
690 1029 $entry['script'],
691 - 'desktopModeNativeWindow_' . str_replace( '-', '_', $entry['id'] ),
1030 + 'openStationNativeWindow_' . str_replace( '-', '_', $entry['id'] ),
692 1031 array(
693 - 'id' => $entry['id'],
694 - 'title' => $entry['title'],
695 - 'icon' => $entry['icon'],
696 - 'width' => $entry['width'],
697 - 'height' => $entry['height'],
698 - 'minWidth' => $entry['min_width'],
699 - 'minHeight' => $entry['min_height'],
700 - 'placement' => $entry['placement'],
701 - 'autofocus' => $entry['autofocus'],
702 - 'templateId' => 'desktop-mode-native-window-' . $entry['id'],
703 - 'tabs' => array_map(
1032 + 'id' => $entry['id'],
1033 + 'title' => $entry['title'],
1034 + 'icon' => $entry['icon'],
1035 + 'width' => $entry['width'],
1036 + 'height' => $entry['height'],
1037 + 'minWidth' => $entry['min_width'],
1038 + 'minHeight' => $entry['min_height'],
1039 + 'placement' => $entry['placement'],
1040 + 'autofocus' => $entry['autofocus'],
1041 + 'templateId' => 'os-native-window-' . $entry['id'],
1042 + 'tabs' => array_map(
704 1043 static function ( $tab ) {
705 1044 return array(
706 1045 'value' => $tab['value'],
707 1046 'label' => $tab['label'],
@@ -712,23 +1051,26 @@
712 1051 ),
713 1052 )
714 1053 );
715 1054
716 - // Bundle-bound `config`. Ships through
717 - // `wp_add_inline_script` `'before'` so it lands on the eager
718 - // path the same way `wp_localize_script` does, AND through
719 - // the lazy-load payload (see `desktop_mode_resolve_script_payload`)
720 - // so the same data is available even when the script is
721 - // dynamically injected mid-session. The bundle reads it via
722 - // `wp.desktop.getWindowConfig( id )` or directly at
723 - // `window.desktopModeWindowConfig[ id ]`.
724 - if ( ! empty( $entry['config'] ) && is_array( $entry['config'] ) ) {
1055 + // Bundle-bound `config`, for the eager print path only.
1056 + // `openstation_build_native_windows_payload()` synthesizes the
1057 + // same assignment into the payload's `scriptL10n`, which is
1058 + // what delivers it on the lazy path — and it has to, because
1059 + // that payload is also built inside chromeless iframes, where
1060 + // this function returns early. Attaching here unconditionally
1061 + // would mean a shell page shipped the identical assignment
1062 + // twice: once as `before`, once as `l10n`. The bundle reads it
1063 + // via `wp.os.getWindowConfig( id )` or directly at
1064 + // `window.openStationWindowConfig[ id ]`.
1065 + $config = openstation_filter_native_window_config( $entry );
1066 + if ( $preload && ! empty( $config ) ) {
725 1067 wp_add_inline_script(
726 1068 $entry['script'],
727 1069 sprintf(
728 - 'window.desktopModeWindowConfig=window.desktopModeWindowConfig||{};window.desktopModeWindowConfig[%s]=%s;',
1070 + 'window.openStationWindowConfig=window.openStationWindowConfig||{};window.openStationWindowConfig[%s]=%s;',
729 1071 wp_json_encode( $entry['id'] ),
730 - wp_json_encode( $entry['config'] )
1072 + wp_json_encode( $config )
731 1073 ),
732 1074 'before'
733 1075 );
734 1076 }
@@ -733,21 +1075,21 @@
733 1075 );
734 1076 }
735 1077 }
736 1078 }
737 -add_action( 'admin_enqueue_scripts', 'desktop_mode_enqueue_native_window_scripts', 20 );
1079 +add_action( 'admin_enqueue_scripts', 'openstation_enqueue_native_window_scripts', 5 );
738 1080
739 1081 /**
740 1082 * Emit a `<template>` tag for every registered native window on
741 1083 * `admin_footer` when the shell is active. The JS side resolves
742 - * these via `document.getElementById( `desktop-mode-native-window-${id}` )`
1084 + * these via `document.getElementById( `os-native-window-${id}` )`
743 1085 * and clones them into each opened window's body.
744 1086 */
745 -function desktop_mode_render_native_window_templates() {
746 - if ( ! desktop_mode_is_enabled() || desktop_mode_is_chromeless_request() || desktop_mode_is_classic_request() ) {
1087 +function openstation_render_native_window_templates() {
1088 + if ( ! openstation_is_shell_request() ) {
747 1089 return;
748 1090 }
749 - $registry = desktop_mode_native_window_registry();
1091 + $registry = openstation_native_window_registry();
750 1092 if ( ! is_array( $registry ) ) {
751 1093 return;
752 1094 }
753 1095 foreach ( $registry as $entry ) {
@@ -753,21 +1095,38 @@
753 1095 foreach ( $registry as $entry ) {
754 1096 if ( ! is_callable( $entry['template'] ) ) {
755 1097 continue;
756 1098 }
757 - $html = desktop_mode_build_native_window_template_html( $entry );
1099 + $html = openstation_build_native_window_template_html( $entry );
758 1100 if ( '' === $html ) {
759 1101 continue;
760 1102 }
761 1103 printf(
762 - '<template id="desktop-mode-native-window-%s">',
1104 + '<template id="os-native-window-%s">',
763 1105 esc_attr( $entry['id'] )
764 1106 );
765 - // `desktop_mode_kses_native_window_template()` auto-extends
766 - // the allowlist with any `<wpd-*>` tag the template carries
1107 + // `openstation_kses_native_window_template()` auto-extends
1108 + // the allowlist with any `<os-*>` tag the template carries
767 1109 // — so plugin authors never have to remember to register
768 1110 // their custom component tags in the kses list.
769 - echo desktop_mode_kses_native_window_template( $html ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper kses-escapes.
1111 + echo openstation_kses_native_window_template( $html ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper kses-escapes.
770 1112 echo '</template>';
771 1113 }
772 1114 }
773 -add_action( 'admin_footer', 'desktop_mode_render_native_window_templates', 20 );
1115 +add_action( 'admin_footer', 'openstation_render_native_window_templates', 20 );
1116 +
1117 +/**
1118 + * Whether a registered window is offered on the admin this request is
1119 + * in: the network admin's shell offers `network` and `any` windows,
1120 + * every site's shell offers `site` and `any`. See the `admin` arg of
1121 + * {@see openstation_register_window()}.
1122 + *
1123 + * @param array<string,mixed> $entry Registry entry.
1124 + * @return bool
1125 + */
1126 +function openstation_native_window_offered_here( $entry ) {
1127 + $admin = isset( $entry['admin'] ) ? (string) $entry['admin'] : 'site';
1128 + if ( 'any' === $admin ) {
1129 + return true;
1130 + }
1131 + return is_network_admin() ? 'network' === $admin : 'site' === $admin;
1132 +}