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 +300 -45 1.0.0 → 1.1.11 View file →
@@ -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,8 +144,48 @@
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 190 * `WP_Error openstation_capability_denied`.
98 191 * @type bool|string $autofocus Passed verbatim to
@@ -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,15 +254,25 @@
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 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 ) ) {
@@ -204,8 +310,17 @@
204 310 $placement = in_array( $args['placement'], array( 'dock', 'none' ), true )
205 311 ? $args['placement']
206 312 : 'dock';
207 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 +
208 323 $entry = array(
209 324 'id' => $id,
210 325 'title' => (string) $args['title'],
211 326 'icon' => (string) $args['icon'],
@@ -210,8 +325,32 @@
210 325 'title' => (string) $args['title'],
211 326 'icon' => (string) $args['icon'],
212 327 'template' => $args['template'],
213 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'],
214 353 'style' => (string) $args['style'],
215 354 'width' => (int) $args['width'],
216 355 'height' => (int) $args['height'],
217 356 'min_width' => (int) $args['min_width'],
@@ -216,8 +355,17 @@
216 355 'height' => (int) $args['height'],
217 356 'min_width' => (int) $args['min_width'],
218 357 'min_height' => (int) $args['min_height'],
219 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'],
220 368 'autofocus' => $args['autofocus'],
221 369 'main_tab_label' => (string) $args['main_tab_label'],
222 370 // Stored as-is (string or int). `openstation_build_native_window_template_html`
223 371 // coerces to int and falls back to 16 when absent.
@@ -225,8 +373,12 @@
225 373 // Bundle-bound config delivered through the same path as
226 374 // `wp_localize_script` `extra['data']` — see the `config` doc
227 375 // in this function's `$args` block and `openstation_resolve_script_payload()`
228 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(),
229 381 'config' => is_array( $args['config'] ) ? $args['config'] : array(),
230 382 );
231 383 openstation_native_window_registry( $id, $entry );
232 384
@@ -284,9 +436,9 @@
284 436 * Templates are inert until JS clones them out of the `<template>`
285 437 * tag — but Plugin Check still requires escape-on-output. The list
286 438 * extends `wp_kses_allowed_html( 'post' )` with form controls,
287 439 * `<os-*>` web components, and dashicon spans, plus permissive
288 - * `data-*`, `aria-*`, and component-specific attributes. Plugins
440 + * `data-*`, common ARIA, and component-specific attributes. Plugins
289 441 * registering their own native windows can extend the list via the
290 442 * `openstation_native_window_allowed_html` filter below.
291 443 *
292 444 * @return array<string,array<string,bool>>
@@ -308,9 +460,15 @@
308 460 'dir' => true,
309 461 'draggable' => true,
310 462 'contenteditable' => true,
311 463 'data-*' => true,
312 - '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,
313 471 // `full-width` is a layout-level flag honoured by
314 472 // `<os-form>` (and any future os-* container that opts in
315 473 // to row-spanning slotted children). Lives in the global
316 474 // allowlist so a plain `<div full-width>` wrapper isn't
@@ -379,8 +537,9 @@
379 537 'bordered' => true,
380 538 'compact' => true,
381 539 'loading' => true,
382 540 'loading-rows' => true,
541 + 'empty' => true,
383 542 'columns' => true,
384 543 'rows' => true,
385 544 'sortable' => true,
386 545 'expandable' => true,
@@ -385,8 +544,9 @@
385 544 'sortable' => true,
386 545 'expandable' => true,
387 546 'preset' => true,
388 547 'label' => true,
548 + 'heading' => true,
389 549 'description' => true,
390 550 'orientation' => true,
391 551 'level' => true,
392 552 'collapsed' => true,
@@ -574,9 +734,9 @@
574 734
575 735 $allowed = array_merge( $base, $extra );
576 736
577 737 // Promote the framework's global attrs (`slot`, `part`,
578 - // `full-width`, `data-*`, `aria-*`, …) to EVERY allowed tag —
738 + // `full-width`, `data-*`, common ARIA, …) to EVERY allowed tag —
579 739 // otherwise plain wrappers like `<div slot="header">` lose
580 740 // their `slot` attribute on the way through kses and get
581 741 // projected into the default slot instead of the named one.
582 742 // Caught by inspection when the Add User form's header
@@ -676,14 +836,23 @@
676 836 call_user_func( $entry['template'] );
677 837 return (string) ob_get_clean();
678 838 }
679 839
680 - // Multi-tab window — wrap in <os-stack> + <os-tabs> + one
681 - // <os-tabpanel> per tab. The default active tab is the main
682 - // one (the window's own template). Plugin authors still get to
683 - // declare their own tab-change side effects via the
684 - // `os-tab-change` event bubbled by <os-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).
685 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 + //
686 855 // The wrap's padding is plugin-controllable two ways:
687 856 // 1. `main_tab_padding` arg on `openstation_register_window` —
688 857 // a per-window override. `0` opts into edge-to-edge
689 858 // content.
@@ -716,29 +885,18 @@
716 885 if ( $padding < 0 ) {
717 886 $padding = 0;
718 887 }
719 888
720 - $buffer = sprintf(
889 + $buffer = sprintf(
721 890 '<os-stack gap="12" padding="%d">',
722 891 $padding
723 892 );
724 - $buffer .= '<os-tabs value="' . esc_attr( OPENSTATION_NATIVE_WINDOW_MAIN_TAB ) . '">';
725 - foreach ( $tabs as $tab ) {
726 - $buffer .= sprintf(
727 - '<os-tab value="%s">%s</os-tab>',
728 - esc_attr( $tab['value'] ),
729 - esc_html( $tab['label'] )
730 - );
731 - }
732 - $buffer .= '</os-tabs>';
733 893
734 894 // Stamp `hidden` on every non-active panel directly in the
735 - // emitted HTML. The client-side `<os-tabs>` syncs panel
736 - // visibility on `value` changes, but its initial sync runs
737 - // inside a microtask — and panel siblings may not have upgraded
738 - // in time on first paint. Setting the attribute server-side
739 - // makes first paint correct regardless of upgrade order; the JS
740 - // 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.
741 899 foreach ( $tabs as $tab ) {
742 900 if ( ! is_callable( $tab['template'] ) ) {
743 901 continue;
744 902 }
@@ -758,15 +916,77 @@
758 916 return $buffer;
759 917 }
760 918
761 919 /**
762 - * Enqueue every registered native window's script when the shell
763 - * is active. Runs on `admin_enqueue_scripts` alongside the main
764 - * shell enqueue so ordering (shell → plugin scripts) is
765 - * 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.
766 931 */
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 + */
767 987 function openstation_enqueue_native_window_scripts() {
768 - if ( ! openstation_is_enabled() || openstation_is_chromeless_request() || openstation_is_classic_request() ) {
988 + if ( ! openstation_is_shell_request() ) {
769 989 return;
770 990 }
771 991 $registry = openstation_native_window_registry();
772 992 if ( ! is_array( $registry ) ) {
@@ -772,12 +992,14 @@
772 992 if ( ! is_array( $registry ) ) {
773 993 return;
774 994 }
775 995 foreach ( $registry as $entry ) {
776 - // Enqueue per-tab scripts — each tab registration can carry
777 - // its own script handle so a tab's JS module stays scoped to
778 - // that tab. Main tab uses the window's own `script`; it's
779 - // enqueued below alongside the localize call.
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`.
780 1002 $tabs = openstation_get_native_window_tabs( $entry['id'] );
781 1003 foreach ( $tabs as $tab ) {
782 1004 if ( $tab['is_main'] || empty( $tab['script'] ) ) {
783 1005 continue;
@@ -787,9 +1009,22 @@
787 1009
788 1010 if ( empty( $entry['script'] ) ) {
789 1011 continue;
790 1012 }
791 - 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 + }
792 1027 // Localize the config the JS side reads to register itself.
793 1028 wp_localize_script(
794 1029 $entry['script'],
795 1030 'openStationNativeWindow_' . str_replace( '-', '_', $entry['id'] ),
@@ -816,23 +1051,26 @@
816 1051 ),
817 1052 )
818 1053 );
819 1054
820 - // Bundle-bound `config`. Ships through
821 - // `wp_add_inline_script` `'before'` so it lands on the eager
822 - // path the same way `wp_localize_script` does, AND through
823 - // the lazy-load payload (see `openstation_resolve_script_payload`)
824 - // so the same data is available even when the script is
825 - // dynamically injected mid-session. The bundle reads it via
826 - // `wp.os.getWindowConfig( id )` or directly at
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
827 1064 // `window.openStationWindowConfig[ id ]`.
828 - if ( ! empty( $entry['config'] ) && is_array( $entry['config'] ) ) {
1065 + $config = openstation_filter_native_window_config( $entry );
1066 + if ( $preload && ! empty( $config ) ) {
829 1067 wp_add_inline_script(
830 1068 $entry['script'],
831 1069 sprintf(
832 1070 'window.openStationWindowConfig=window.openStationWindowConfig||{};window.openStationWindowConfig[%s]=%s;',
833 1071 wp_json_encode( $entry['id'] ),
834 - wp_json_encode( $entry['config'] )
1072 + wp_json_encode( $config )
835 1073 ),
836 1074 'before'
837 1075 );
838 1076 }
@@ -837,9 +1075,9 @@
837 1075 );
838 1076 }
839 1077 }
840 1078 }
841 -add_action( 'admin_enqueue_scripts', 'openstation_enqueue_native_window_scripts', 20 );
1079 +add_action( 'admin_enqueue_scripts', 'openstation_enqueue_native_window_scripts', 5 );
842 1080
843 1081 /**
844 1082 * Emit a `<template>` tag for every registered native window on
845 1083 * `admin_footer` when the shell is active. The JS side resolves
@@ -846,9 +1084,9 @@
846 1084 * these via `document.getElementById( `os-native-window-${id}` )`
847 1085 * and clones them into each opened window's body.
848 1086 */
849 1087 function openstation_render_native_window_templates() {
850 - if ( ! openstation_is_enabled() || openstation_is_chromeless_request() || openstation_is_classic_request() ) {
1088 + if ( ! openstation_is_shell_request() ) {
851 1089 return;
852 1090 }
853 1091 $registry = openstation_native_window_registry();
854 1092 if ( ! is_array( $registry ) ) {
@@ -874,4 +1112,21 @@
874 1112 echo '</template>';
875 1113 }
876 1114 }
877 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 +}