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