PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.9
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.9
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 0.8.6 All 33 releases
desktop-mode / includes / assets.php

assets.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.9, at includes/assets.php

563 lines 22.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation asset registration.
4 *
5 * Registers all openstation CSS and JS handles with WordPress so they can
6 * be enqueued from anywhere in the plugin (or by third parties).
7 *
8 * @package OpenStation
9 */
10
11 defined( 'ABSPATH' ) || exit;
12
13 /**
14 * Cache-buster for a stylesheet that `@import`s sub-sheets: the max
15 * `filemtime` across the file AND everything it (transitively) imports.
16 * Sub-sheet URLs carry no `?ver=` of their own, so the parent's stamp is
17 * the only cache key the browser ever sees for the subtree — it must
18 * move when any member changes.
19 *
20 * @param string $relative Stylesheet path relative to the plugin dir.
21 * @param string $fallback Version to use when the file is missing.
22 * @return string Version string.
23 */
24 function openstation_css_subtree_version( $relative, $fallback ) {
25 $root = OPENSTATION_DIR . $relative;
26 if ( ! file_exists( $root ) ) {
27 return (string) $fallback;
28 }
29 $max = (int) filemtime( $root );
30 $queue = array( $root );
31 $seen = array( $root => true );
32 while ( ! empty( $queue ) ) {
33 $file = array_pop( $queue );
34 $css = (string) file_get_contents( $file );
35 if ( ! preg_match_all( '/@import\s+url\(\s*["\']?([^"\')\s]+)["\']?\s*\)/i', $css, $matches ) ) {
36 continue;
37 }
38 foreach ( $matches[1] as $import ) {
39 $path = dirname( $file ) . '/' . $import;
40 if ( ! file_exists( $path ) || isset( $seen[ $path ] ) ) {
41 continue;
42 }
43 $seen[ $path ] = true;
44 $max = max( $max, (int) filemtime( $path ) );
45 $queue[] = $path;
46 }
47 }
48 return (string) $max;
49 }
50
51 /**
52 * Registers the OpenStation CSS and JS handles.
53 */
54 function openstation_register_assets() {
55 $version = OPENSTATION_VERSION;
56 $suffix = openstation_asset_suffix();
57
58 // `filemtime`-stamped version for built bundles. The plugin-wide
59 // `OPENSTATION_VERSION` is bumped per release, but the bundles
60 // iterate faster — without a per-file mtime stamp, two clients
61 // loading the same `?ver=…` URL can be served different bytes
62 // (whichever build was on disk at upload time). Stamping with the
63 // file's modification time guarantees the URL changes whenever the
64 // file does, so "is my fix deployed?" is answerable from the
65 // network tab. Falls back to `$version` when the file is missing
66 // (test envs that import this file before the build runs).
67 $built_version = static function ( $relative ) use ( $version ) {
68 $path = OPENSTATION_DIR . $relative;
69 return file_exists( $path ) ? (string) filemtime( $path ) : $version;
70 };
71
72 // Styles.
73 // `filemtime`-stamped, NOT the plugin-wide `$version`. This file
74 // is the token catalogue every other sheet resolves `var()`s
75 // against, and it changes whenever the palette does — which is a
76 // lot more often than the plugin version is bumped. With a static
77 // stamp the browser holds the old palette until a hard reload,
78 // and the symptom is maddening: a themed shell where some
79 // surfaces update and others don't.
80 wp_register_style(
81 'os-variables',
82 OPENSTATION_URL . 'assets/css/variables.css',
83 array(),
84 $built_version( 'assets/css/variables.css' )
85 );
86 // `filemtime`-stamped so the `<link rel="stylesheet">` URL matches the
87 // `<link rel="preload" as="style">` hint emitted by
88 // `openstation_print_preload_hints()` (which stamps with filemtime).
89 // Registering this with the plain `$version` instead produced two
90 // different `?ver=` query strings for the same file, so the browser
91 // never matched the preload to the stylesheet and logged "preloaded
92 // but not used within a few seconds from the window's load event".
93 wp_register_style(
94 'openstation',
95 OPENSTATION_URL . 'assets/css/desktop.css',
96 array( 'os-variables' ),
97 $built_version( 'assets/css/desktop.css' )
98 );
99
100 /*
101 * Window styles — one handle per sheet, chained by dependency.
102 *
103 * These used to be `@import url( … )`ed from `windows.css` under
104 * the single `os-windows` handle. That was a standing
105 * cache bug: an `@import` URL carries no `?ver=`, so a changed
106 * sub-sheet had no URL for the browser to invalidate. Stamping the
107 * PARENT with the subtree's max mtime (which is what
108 * `openstation_css_subtree_version()` was for) made the browser
109 * re-fetch `windows.css` and then request each sub-sheet at an
110 * unchanged URL — free to be served from its heuristic cache. The
111 * result was edits not landing until a hard refresh, and rules
112 * being relocated into `windows.css` purely to dodge it.
113 *
114 * Now every sheet is separately registered with its own
115 * `filemtime` stamp, so each has a real cache key.
116 *
117 * THE DEPENDENCY CHAIN IS LOAD-BEARING. WordPress prints
118 * dependencies before dependents, so chaining each sheet to the
119 * previous one reproduces the order the `@import` block had, and
120 * `os-windows` (which depends on the last link) still
121 * prints after them and still wins ties:
122 *
123 * window-chrome → window-states → effects → window-links
124 * → windows
125 *
126 * `window-overview` is deliberately NOT in this chain — it is
127 * registered below, after `os-windows`, so it can load deferred.
128 * Adding a sheet here means splicing it into the chain, not
129 * appending an unrelated dependency: order is the contract.
130 * (The Preferences window's sheet is not here at all: it rides
131 * the `apps/os-settings/` app as a first-open companion style.)
132 */
133 $window_sheets = array(
134 'os-window-chrome' => 'assets/css/window-chrome.css',
135 'os-window-states' => 'assets/css/window-states.css',
136 'os-effects' => 'assets/css/effects.css',
137 'os-window-links' => 'assets/css/window-links.css',
138 );
139 $previous = array( 'os-variables', 'dashicons' );
140 foreach ( $window_sheets as $handle => $relative ) {
141 wp_register_style(
142 $handle,
143 OPENSTATION_URL . $relative,
144 $previous,
145 $built_version( $relative )
146 );
147 $previous = array( $handle );
148 }
149
150 // Entry point. Depends on the tail of the chain above, so
151 // enqueuing this one handle still pulls in every critical window
152 // sheet — the behaviour callers had when they were `@import`s.
153 wp_register_style(
154 'os-windows',
155 OPENSTATION_URL . 'assets/css/windows.css',
156 $previous,
157 $built_version( 'assets/css/windows.css' )
158 );
159 // Loads DEFERRED (see `openstation_defer_non_critical_styles()`):
160 // the UI it styles — the window overview — is lazy-loaded JS that
161 // can never be on screen at first paint, so its CSS has no
162 // business blocking render. It depends on `os-windows` so it
163 // prints after it, preserving the cascade position it had as an
164 // `@import`.
165 wp_register_style(
166 'os-window-overview',
167 OPENSTATION_URL . 'assets/css/window-overview.css',
168 array( 'os-windows' ),
169 $built_version( 'assets/css/window-overview.css' )
170 );
171 // Solo mode — one window, no desk around it. Loads last so it can
172 // hide surfaces the sheets above declared, and only enqueues on a
173 // solo request, so a normal shell never pays for it.
174 wp_register_style(
175 'os-solo',
176 OPENSTATION_URL . 'assets/css/solo.css',
177 array( 'os-windows' ),
178 $built_version( 'assets/css/solo.css' )
179 );
180 wp_register_style(
181 'os-dock',
182 OPENSTATION_URL . 'assets/css/dock.css',
183 array( 'os-variables', 'dashicons' ),
184 $built_version( 'assets/css/dock.css' )
185 );
186 wp_register_style(
187 'os-dock-peek',
188 OPENSTATION_URL . 'assets/css/dock-peek.css',
189 array( 'os-dock' ),
190 $built_version( 'assets/css/dock-peek.css' )
191 );
192 // The phone layer. Every rule is scoped to
193 // `html[data-os-mode="mobile"]`, so the sheet is inert on a
194 // desktop and the first paint on a phone is already right (the
195 // head stamp writes the attribute before any stylesheet applies).
196 // Depends on the dock and window sheets because it overrides them.
197 wp_register_style(
198 'os-mobile',
199 OPENSTATION_URL . 'assets/css/mobile.css',
200 array( 'os-variables', 'dashicons', 'os-dock', 'os-windows' ),
201 $built_version( 'assets/css/mobile.css' )
202 );
203 // The notch — the shell's top-centre voice and the site
204 // assistant's front door. Scoped to `.os-notch`.
205 wp_register_style(
206 'os-notch',
207 OPENSTATION_URL . 'assets/css/notch.css',
208 array( 'os-variables' ),
209 $built_version( 'assets/css/notch.css' )
210 );
211 // The workspace wizard. Scoped to `.os-workspace-wizard`, so it is
212 // inert until the user opens it from the overview bar's `+` or a
213 // tile's Edit. Those controls are styled with the rest of the bar
214 // in `window-overview.css`.
215 wp_register_style(
216 'os-workspaces',
217 OPENSTATION_URL . 'assets/css/workspaces.css',
218 array( 'os-variables', 'dashicons' ),
219 $built_version( 'assets/css/workspaces.css' )
220 );
221 // Keyboard-shortcuts window. Scoped to `.os-shortcuts`, so it is
222 // inert until the System menu opens the window, and unconditional
223 // for the same reason the layout sheet is: the window can be
224 // opened at any moment and a deferred sheet would paint it raw.
225 wp_register_style(
226 'os-shortcuts',
227 OPENSTATION_URL . 'assets/css/shortcuts.css',
228 array( 'os-variables' ),
229 $built_version( 'assets/css/shortcuts.css' )
230 );
231 // The OpenStation desktop layout — the core/plugin seam on the rail
232 // plus the constellation hover-submenu flyout. Both surfaces are
233 // scoped (`[data-os-layout="openstation"]` / `.os-constellation`),
234 // so the sheet is inert in every other layout and is loaded
235 // unconditionally rather than gated on the user's current pick:
236 // the layout is switchable live from OpenStation Preferences, and a
237 // conditional enqueue would leave the first switch unstyled.
238 wp_register_style(
239 'os-openstation-layout',
240 OPENSTATION_URL . 'assets/css/openstation-layout.css',
241 array( 'os-dock' ),
242 $built_version( 'assets/css/openstation-layout.css' )
243 );
244 // `filemtime`-stamped — the chromeless overrides iterate faster
245 // than the plugin-wide version bumps (per-page compat shims and
246 // page-title-action exceptions land in patches), and a stale
247 // cached copy means the user sees yesterday's rules. Without the
248 // stamp, the browser keeps `?ver=0.8.1` valid for the whole
249 // release cycle.
250 wp_register_style(
251 'os-chromeless',
252 OPENSTATION_URL . 'assets/css/chromeless.css',
253 array( 'openstation' ),
254 $built_version( 'assets/css/chromeless.css' )
255 );
256
257 // `filemtime`-stamped — the AI assistant surface (error states,
258 // inline affordances) iterates faster than plugin version bumps.
259 // Without an mtime stamp the browser keeps `?ver=<plugin-version>`
260 // valid for the whole release cycle and the user keeps seeing
261 // yesterday's CSS even after a hard reload.
262 wp_register_style(
263 'desktop-mode-ai-assistant',
264 OPENSTATION_URL . 'assets/css/ai-assistant.css',
265 array( 'os-variables' ),
266 $built_version( 'assets/css/ai-assistant.css' )
267 );
268
269 wp_register_style(
270 'desktop-mode-bug-report',
271 OPENSTATION_URL . 'assets/css/bug-report.css',
272 array( 'os-variables' ),
273 $built_version( 'assets/css/bug-report.css' )
274 );
275
276 // `filemtime` instead of the plugin-wide `$version` for the
277 // recycle-bin CSS — this file iterates faster than the bundle
278 // and we never want a stale CSS cache to mask a real fix.
279 $recycle_bin_css = OPENSTATION_DIR . 'assets/css/recycle-bin.css';
280 wp_register_style(
281 'desktop-mode-recycle-bin',
282 OPENSTATION_URL . 'assets/css/recycle-bin.css',
283 array( 'os-variables', 'dashicons' ),
284 file_exists( $recycle_bin_css ) ? (string) filemtime( $recycle_bin_css ) : $version
285 );
286
287 // Files-on-the-Desktop tile + layer styles. `filemtime` for the
288 // same reason as the recycle-bin CSS: this file
289 // iterates faster than the plugin version, and a stale cache
290 // would mask a real fix.
291 $desktop_files_css = OPENSTATION_DIR . 'assets/css/desktop-files.css';
292 wp_register_style(
293 'os-files',
294 OPENSTATION_URL . 'assets/css/desktop-files.css',
295 array( 'os-variables', 'dashicons' ),
296 file_exists( $desktop_files_css ) ? (string) filemtime( $desktop_files_css ) : $version
297 );
298
299 // Games hub window (launcher grid, scoreboard, challenges) +
300 // per-game styles. Same `filemtime` cache-bust posture as the
301 // other fast-iterating feature stylesheets.
302 $games_css = OPENSTATION_DIR . 'assets/css/games.css';
303 wp_register_style(
304 'desktop-mode-games',
305 OPENSTATION_URL . 'assets/css/games.css',
306 array( 'os-variables', 'dashicons' ),
307 file_exists( $games_css ) ? (string) filemtime( $games_css ) : $version
308 );
309 $game_inkfall_css = OPENSTATION_DIR . 'assets/css/game-inkfall.css';
310 wp_register_style(
311 'os-game-inkfall',
312 OPENSTATION_URL . 'assets/css/game-inkfall.css',
313 array( 'os-variables' ),
314 file_exists( $game_inkfall_css ) ? (string) filemtime( $game_inkfall_css ) : $version
315 );
316 $game_alphabet_soup_css = OPENSTATION_DIR . 'assets/css/game-alphabet-soup.css';
317 wp_register_style(
318 'os-game-alphabet-soup',
319 OPENSTATION_URL . 'assets/css/game-alphabet-soup.css',
320 array( 'os-variables' ),
321 file_exists( $game_alphabet_soup_css ) ? (string) filemtime( $game_alphabet_soup_css ) : $version
322 );
323
324 // Pinned-notes layer styles (paper, pushpin, pastel tokens, pin
325 // animations). Same `filemtime` cache-bust posture as the other
326 // fast-iterating feature stylesheets above. Depends on `os-files`
327 // because a pinned note dresses the canonical `.os-file-tile`
328 // chrome — anything that restyles a tile has to print after the
329 // file that declares one.
330 $notes_css = OPENSTATION_DIR . 'assets/css/notes.css';
331 wp_register_style(
332 'os-notes',
333 OPENSTATION_URL . 'assets/css/notes.css',
334 array( 'os-variables', 'dashicons', 'os-files' ),
335 file_exists( $notes_css ) ? (string) filemtime( $notes_css ) : $version
336 );
337
338 // Announcement-dialog styles (scrim, hero card, buttons). Registered
339 // always, enqueued only for a user who is actually owed the
340 // announcement — see the gate in `openstation_enqueue_assets()`. It
341 // is not lazy-loaded on top of that: on the one boot where it does
342 // go out, the dialog opens from the main bundle a second later, and
343 // a fetch racing that would show an unstyled card.
344 $announce_css = OPENSTATION_DIR . 'assets/css/announce.css';
345 wp_register_style(
346 'os-announce',
347 OPENSTATION_URL . 'assets/css/announce.css',
348 array( 'os-variables' ),
349 file_exists( $announce_css ) ? (string) filemtime( $announce_css ) : $version
350 );
351
352 // Scripts.
353 //
354 // `wp-hooks` — the shell exposes a WordPress-style filter/action
355 // API (`window.wp.hooks`) to third-party plugins.
356 // `wp-i18n` — the TS `__()` / `_x()` / `sprintf()` wrappers in
357 // `src/i18n.ts` delegate to `window.wp.i18n` for translation
358 // lookups. Both handles are core-shipped but only pre-enqueued
359 // when Gutenberg-adjacent deps pull them in, so we list them
360 // explicitly to guarantee load order.
361 wp_register_script(
362 'openstation',
363 OPENSTATION_URL . 'assets/js/desktop' . $suffix . '.js',
364 // `heartbeat` + `jquery` — the recycle-bin badge module
365 // (loaded as part of this bundle) opts into the WordPress
366 // Heartbeat API so the count tile / desktop-icon badge
367 // stays in sync even when the bin window is closed.
368 array( 'wp-hooks', 'wp-i18n', 'heartbeat', 'jquery' ),
369 $built_version( 'assets/js/desktop' . $suffix . '.js' ),
370 // Footer + defer: the shell boots on DOMContentLoaded anyway,
371 // so deferring frees the parser instead of blocking at the
372 // footer print point. Both inline payloads attached to this
373 // handle (`__openStationMenuCommands`, the jazz-quote version
374 // stamp) are `'before'`-position, which WP keeps as blocking
375 // inline ahead of a deferred tag — order is preserved. On WP
376 // < 6.3 the array collapses to a truthy `$in_footer`, same as
377 // before.
378 array(
379 'in_footer' => true,
380 'strategy' => 'defer',
381 )
382 );
383
384 // `os-iframe-bridge` — opt-in iframe-side bridge that
385 // provides `wp.os.iframe.publish/subscribe/onConnection/
386 // requestConnection` to any same-origin iframe that enqueues it.
387 // Same code is also injected inline by the chromeless bridge
388 // (so chromeless wp-admin pages don't need a separate enqueue)
389 // and auto-injected when a native window opts in via
390 // `iframeContent: { bridge: true }`. Plugins targeting their
391 // own iframe pages just enqueue this handle.
392 wp_register_script(
393 'os-iframe-bridge',
394 OPENSTATION_URL . 'assets/js/iframe-bridge' . $suffix . '.js',
395 array(),
396 $built_version( 'assets/js/iframe-bridge' . $suffix . '.js' ),
397 true
398 );
399
400 // `os-chromeless-bridge` — the iframe side of every window,
401 // enqueued on `admin_footer` by
402 // `openstation_chromeless_bridge_script()` with its per-request
403 // data attached as a `before` inline block. Registered here (not
404 // there) so the handle exists before the enqueue and so the asset
405 // guard's OPENSTATION_URL snapshot protects it like every other
406 // bundle of ours.
407 wp_register_script(
408 'os-chromeless-bridge',
409 OPENSTATION_URL . 'assets/js/chromeless-bridge' . $suffix . '.js',
410 array(),
411 $built_version( 'assets/js/chromeless-bridge' . $suffix . '.js' ),
412 true
413 );
414
415 // `os-gutenberg-drop-receiver` — iframe-side bundle
416 // enqueued only on the Block Editor screens (`post.php` /
417 // `post-new.php`) for openstation users. Listens for
418 // `os-drop` postMessages from the parent shell (see
419 // `src/drag/iframe-drop-targets.ts`) and inserts the matching
420 // Gutenberg block via `wp.data.dispatch('core/block-editor')`.
421 // Depends on `wp-blocks` + `wp-data` so the editor stores are
422 // guaranteed enqueued before the receiver runs its first message
423 // handler.
424 wp_register_script(
425 'os-gutenberg-drop-receiver',
426 OPENSTATION_URL . 'assets/js/gutenberg-drop-receiver' . $suffix . '.js',
427 array( 'wp-blocks', 'wp-data' ),
428 $built_version( 'assets/js/gutenberg-drop-receiver' . $suffix . '.js' ),
429 true
430 );
431
432 // The Recycle Bin window is an App Framework app (`apps/trash/`);
433 // its client view rides the app-bundle pipeline, so no dedicated
434 // script handle. The `desktop-mode-recycle-bin` STYLE handle above
435 // stays — the drag-to-trash drop-target highlight must be present
436 // at boot (dropping on the closed bin's dock tile), not window-open.
437
438 // `desktop-mode-games` — bundle for the Games hub native window
439 // (launcher grid, scoreboard, challenges client). Lazy-loaded by
440 // the native-window sync the first time the hub opens; registers a
441 // render callback on
442 // `window.openStationNativeWindows['desktop-mode-games']`.
443 // `heartbeat` + `jquery` — the challenges client rides the
444 // WordPress Heartbeat bus for live delivery.
445 $games_js = OPENSTATION_DIR . 'assets/js/games' . $suffix . '.js';
446 wp_register_script(
447 'desktop-mode-games',
448 OPENSTATION_URL . 'assets/js/games' . $suffix . '.js',
449 array( 'wp-i18n', 'heartbeat', 'jquery' ),
450 file_exists( $games_js ) ? (string) filemtime( $games_js ) : $version,
451 true
452 );
453 wp_set_script_translations(
454 'desktop-mode-games',
455 'desktop-mode',
456 OPENSTATION_DIR . 'languages'
457 );
458
459 // `os-game-inkfall` — the Inkfall game bundle. Loaded
460 // lazily by the games framework on first launch; publishes the
461 // game def on `window.openStationGames.inkfall`.
462 $game_inkfall_js = OPENSTATION_DIR . 'assets/js/game-inkfall' . $suffix . '.js';
463 wp_register_script(
464 'os-game-inkfall',
465 OPENSTATION_URL . 'assets/js/game-inkfall' . $suffix . '.js',
466 array( 'wp-i18n' ),
467 file_exists( $game_inkfall_js ) ? (string) filemtime( $game_inkfall_js ) : $version,
468 true
469 );
470 wp_set_script_translations(
471 'os-game-inkfall',
472 'desktop-mode',
473 OPENSTATION_DIR . 'languages'
474 );
475
476 // `os-game-alphabet-soup` — the Alphabet Soup game
477 // bundle. Loaded lazily by the games framework on first launch;
478 // publishes the game def on
479 // `window.openStationGames['alphabet-soup']`.
480 $game_alphabet_soup_js = OPENSTATION_DIR . 'assets/js/game-alphabet-soup' . $suffix . '.js';
481 wp_register_script(
482 'os-game-alphabet-soup',
483 OPENSTATION_URL . 'assets/js/game-alphabet-soup' . $suffix . '.js',
484 array( 'wp-i18n' ),
485 file_exists( $game_alphabet_soup_js ) ? (string) filemtime( $game_alphabet_soup_js ) : $version,
486 true
487 );
488 wp_set_script_translations(
489 'os-game-alphabet-soup',
490 'desktop-mode',
491 OPENSTATION_DIR . 'languages'
492 );
493
494 // `os-animated-logo-wallpaper` — built-in PixiJS canvas
495 // wallpaper, moved out of `desktop.min.js`. The wallpaper
496 // `server-sync` loads this handle when the user selects the
497 // `wp-animated-logo` wallpaper (or opens OS Settings → Wallpaper
498 // and the picker pulls every registered canvas def in). The
499 // bundle's only side effect is publishing the `WallpaperDef` on
500 // `window.openStationWallpapers['wp-animated-logo']`.
501 $animated_logo_js = OPENSTATION_DIR . 'assets/js/animated-logo-wallpaper' . $suffix . '.js';
502 wp_register_script(
503 'os-animated-logo-wallpaper',
504 OPENSTATION_URL . 'assets/js/animated-logo-wallpaper' . $suffix . '.js',
505 array( 'wp-hooks' ),
506 file_exists( $animated_logo_js ) ? (string) filemtime( $animated_logo_js ) : $version,
507 true
508 );
509
510 // `os-snow-wallpaper` — built-in PixiJS canvas
511 // wallpaper: snowfall that accumulates on window tops and melts
512 // away. Same lazy-load path as the animated logo: the wallpaper
513 // `server-sync` injects this handle when the user selects the
514 // `wp-snow` wallpaper (or opens OS Settings → Wallpaper and the
515 // picker pulls the def in). The bundle's only side effect is
516 // publishing the `WallpaperDef` on
517 // `window.openStationWallpapers['wp-snow']`.
518 $snow_js = OPENSTATION_DIR . 'assets/js/snow-wallpaper' . $suffix . '.js';
519 wp_register_script(
520 'os-snow-wallpaper',
521 OPENSTATION_URL . 'assets/js/snow-wallpaper' . $suffix . '.js',
522 array( 'wp-hooks', 'wp-i18n' ),
523 file_exists( $snow_js ) ? (string) filemtime( $snow_js ) : $version,
524 true
525 );
526 wp_set_script_translations(
527 'os-snow-wallpaper',
528 'desktop-mode',
529 OPENSTATION_DIR . 'languages'
530 );
531
532 // `desktop-mode-ai-assistant` — AI Copilot spotlight overlay,
533 // moved out of `desktop.min.js`. The main bundle ships a
534 // stub matching the public `wp.os.ai` contract; the stub
535 // `<script>`-injects this handle the first time the user opens
536 // the assistant (Cmd+K or admin-bar button).
537 $ai_assistant_js = OPENSTATION_DIR . 'assets/js/ai-assistant' . $suffix . '.js';
538 wp_register_script(
539 'desktop-mode-ai-assistant',
540 OPENSTATION_URL . 'assets/js/ai-assistant' . $suffix . '.js',
541 array( 'wp-hooks', 'wp-i18n' ),
542 file_exists( $ai_assistant_js ) ? (string) filemtime( $ai_assistant_js ) : $version,
543 true
544 );
545 wp_set_script_translations(
546 'desktop-mode-ai-assistant',
547 'desktop-mode',
548 OPENSTATION_DIR . 'languages'
549 );
550
551 // Wire the translation bundle to this script handle. WP looks
552 // for `languages/os-{locale}-os.json` and
553 // injects its `locale_data` into `wp.i18n` just before the
554 // script runs — so every `__()` call resolves to the right
555 // language without any runtime fetch.
556 wp_set_script_translations(
557 'openstation',
558 'desktop-mode',
559 OPENSTATION_DIR . 'languages'
560 );
561 }
562 add_action( 'init', 'openstation_register_assets' );
563