PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.13
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.13
1.1.13 1.1.12 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 All 37 releases
← All changes | includes/os-settings.php +289 -45 1.1.0 → 1.1.13 View file →
@@ -30,8 +30,11 @@
30 30
31 31 /** Valid window-radius IDs — mirrors the TS `WINDOW_RADII` constant. */
32 32 const OPENSTATION_OS_SETTINGS_WINDOW_RADII = array( 'sharp', 'default', 'round' );
33 33
34 +/** How a newly opened window lands — mirrors the TS `OPEN_WINDOWS_AS` constant. */
35 +const OPENSTATION_OS_SETTINGS_OPEN_WINDOWS_AS = array( 'default', 'maximized', 'focused' );
36 +
34 37 /**
35 38 * Valid admin-bar mode IDs — mirrors the TS `ADMIN_BAR_MODES` constant.
36 39 *
37 40 * `static` keeps the WordPress admin bar pinned above the shell (the
@@ -52,8 +55,28 @@
52 55 */
53 56 const OPENSTATION_OS_SETTINGS_DOCK_PLACEMENTS = array( 'bottom', 'left', 'right' );
54 57
55 58 /**
59 + * Valid dock-behavior IDs — mirrors the TS `DOCK_BEHAVIORS` constant.
60 + *
61 + * `static` keeps the dock always on screen (the default); `dynamic`
62 + * parks it off its edge behind a peek strip that reveals when the
63 + * pointer reaches that edge or something on it takes keyboard focus,
64 + * and releases the band it floats over from the work area.
65 + */
66 +const OPENSTATION_OS_SETTINGS_DOCK_BEHAVIORS = array( 'static', 'dynamic' );
67 +
68 +/**
69 + * `mobileLayout` — which experience the shell renders. `auto`
70 + * follows the viewport; the other two force it either way.
71 + * Mirrors `OsModePreference` in `src/mode/index.ts`.
72 + */
73 +const OPENSTATION_OS_SETTINGS_MOBILE_LAYOUTS = array( 'auto', 'desktop', 'mobile' );
74 +
75 +/** `mobileTabs` — at most this many ids pinned to the phone tab bar. */
76 +const OPENSTATION_OS_SETTINGS_MOBILE_TABS_MAX = 3;
77 +
78 +/**
56 79 * Playable range for the window-reveal duration override, in ms.
57 80 * Mirrors `MIN_REVEAL_DURATION_MS` / `MAX_REVEAL_DURATION_MS` in
58 81 * `src/reveals/registry.ts`.
59 82 *
@@ -73,8 +96,10 @@
73 96 */
74 97 function openstation_default_os_settings() {
75 98 return array(
76 99 'wallpaper' => 'galaxy',
100 + // Pulse, the brand's signature. Mirrors `DEFAULTS` in
101 + // `src/settings/constants.ts`.
77 102 'accent' => 'pulse',
78 103 // Only read when `accent` is `custom`. Seeded with Pulse so
79 104 // picking Custom before touching the wheel is a no-op rather
80 105 // than a jump to black. Mirrors `DEFAULTS` in
@@ -88,8 +113,9 @@
88 113 // `src/settings/constants.ts` — PHP seeds the first load and JS
89 114 // owns every paint after it, so a mismatch shows up as the
90 115 // corners changing shape a moment after the shell boots.
91 116 'windowRadius' => 'round',
117 + 'openWindowsAs' => 'default',
92 118 // How the WordPress admin bar presents above the shell.
93 119 // `hidden` ships as the default so a fresh desktop has ONE
94 120 // navigation surface: everything the user can open lives on the
95 121 // dock, and the dock's "Exit OpenStation" tile is the way back
@@ -95,8 +121,14 @@
95 121 // dock, and the dock's "Exit OpenStation" tile is the way back
96 122 // to classic admin. `static` (vanilla behavior) and `dynamic`
97 123 // are one pick away in OpenStation Preferences → Appearance.
98 124 'adminBarMode' => 'hidden',
125 + // Always on screen. `dynamic` (auto-hide behind a peek strip)
126 + // is one pick away in OpenStation Preferences → Appearance.
127 + 'dockBehavior' => 'static',
128 + // The Split layout's sidebar answers for itself: a folded
129 + // sidebar over a static bottom dock is a valid desk.
130 + 'sideDockBehavior' => 'static',
99 131 // One dock holding every menu, with the system tiles grouped
100 132 // behind a hairline. `classic` (side bar for core menus + bottom
101 133 // dock for plugins) is the other option; it is no longer what a
102 134 // first-run desktop looks like.
@@ -184,8 +216,13 @@
184 216 // window (e.g. array( 'author', 'tags' )). Empty array means
185 217 // every column is visible. The sticky 'title' column is always
186 218 // shown — the UI prevents toggling it.
187 219 'nativePostsHiddenColumns' => array(),
220 + // Per-user list of column keys hidden in the native Pages
221 + // window (e.g. array( 'author', 'parent' )). Empty array means
222 + // every column is visible. The sticky 'title' column is always
223 + // shown — the UI prevents toggling it.
224 + 'nativePagesHiddenColumns' => array(),
188 225 // Per-user opt-IN for the native Pages window. Same posture as
189 226 // nativePostsEnabled — defaults OFF (Beta), users opt in to swap
190 227 // the classic `edit.php?post_type=page` iframe for the native UI.
191 228 'nativePagesEnabled' => false,
@@ -202,8 +239,17 @@
202 239 // Per-user opt-IN for the native Comments window. Defaults OFF
203 240 // (Beta); the server-side cap gate (`edit_posts`) means the
204 241 // toggle only matters for users who could see the Comments tile.
205 242 'nativeCommentsEnabled' => false,
243 + // Per-user opt-IN for Station Home, the native Dashboard
244 + // window. Defaults OFF: the ordinary `index.php` Dashboard
245 + // (including any custom dashboard a plugin builds there) opens
246 + // as a chromeless iframe until the user opts in via OS
247 + // Settings → Features → Beta features.
248 + 'stationHomeEnabled' => false,
249 + // Performance enhancements are enabled unless explicitly disabled.
250 + 'adminAssetCacheEnabled' => true,
251 + 'windowPrewarmEnabled' => true,
206 252 // When true, left-clicking the empty wallpaper triggers the
207 253 // "Show desktop" toggle (macOS-style) and the matching entry is
208 254 // hidden from the wallpaper context menu. When false (default),
209 255 // the entry stays in the menu and left clicks on the wallpaper
@@ -208,13 +254,20 @@
208 254 // hidden from the wallpaper context menu. When false (default),
209 255 // the entry stays in the menu and left clicks on the wallpaper
210 256 // do nothing. Per-user.
211 257 'showDesktopOnWallpaperClick' => false,
258 + // Whether the close-all-windows shortcut (Alt+Cmd/Ctrl+W) asks
259 + // before it closes. The dialog's "Don't ask again" checkbox is
260 + // what writes false; OpenStation Preferences -> Windows is what
261 + // turns it back on. Per-user.
262 + 'confirmCloseAllWindows' => true,
212 263 // Mio — a soft-body companion that floats over
213 264 // the wallpaper, settles onto nearby windows, and watches the
214 265 // pointer. Off by default; toggled from the wallpaper context
215 266 // menu. Per-user. See `docs/mio.md`.
216 267 'mioEnabled' => false,
268 + 'mioApiEnabled' => false,
269 + 'mioShowOnWallpaper' => true,
217 270 // The user's own Mio, as built in "Make it yours": partial
218 271 // appearance + silhouette overrides, both empty until they
219 272 // touch a control. Stored per user rather than per browser
220 273 // because it is a preference about the person — ten minutes
@@ -231,11 +284,12 @@
231 284 // a glance. Per-user.
232 285 'showPostStatusRibbons' => true,
233 286 // Unlocks developer-facing surfaces meant for plugin
234 287 // authors: the Starter Widget appears in the add-widget
235 - // picker, and the OS Settings → Components tab runs its
236 - // intentional missing-import-warner demo. Off by default.
237 - // Per-user.
288 + // picker, the OS Settings → Components tab runs its
289 + // intentional missing-import-warner demo, and the Code Blue
290 + // error-log reader registers (icon, window, REST routes).
291 + // Off by default. Per-user.
238 292 'developerModeEnabled' => false,
239 293 // Per-user opt-OUT for the folder-sharing feature. Defaults
240 294 // ON. When false:
241 295 // - The Share button, share-settings modal, "Leave shared
@@ -250,23 +304,30 @@
250 304 // disappears without any database changes. The site-wide
251 305 // "Delete folder sharing data" action in OS Settings →
252 306 // Features → Advanced is a separate destructive cleanup.
253 307 'foldersSharingEnabled' => true,
254 - // Per-item placement preferences. Map of item id (dock-item
255 - // slug or registered desktop-icon id) → one of:
256 - // 'both' — show on both dock and desktop.
257 - // 'dock' — show only on the dock; hide from desktop.
258 - // 'desktop' — show only on the wallpaper; hide from dock.
308 + // Per-item navigation placement. Map of item id → one of:
309 + // 'both' — show on a rail and on the desktop.
310 + // 'rail' — show only on a rail: the dock, or the sidebar
311 + // for a Core admin menu in the split layout.
312 + // 'desktop' — show only on the wallpaper.
259 313 // 'hidden' — hide from every shell surface.
260 - // Missing keys mean "no override" — items use their native rail.
314 + // Missing keys mean "no override" — the item takes the default
315 + // for its kind, which lives in `src/nav/defaults.ts`.
261 316 // Sanitized as map<sanitize_key, enum>. Capped at 256 entries.
262 - 'itemVisibility' => array(),
263 - // Per-user dock ordering. Ordered list of item ids; ids not in
264 - // the list keep their server-supplied position appended after
265 - // the listed ones. Unknown ids are tolerated.
266 - 'dockOrder' => array(),
267 - // Persisted desktop position for every dock item the user has
268 - // promoted to the wallpaper via `itemVisibility[id]=desktop|both`.
317 + 'navPlacement' => array(),
318 + // Per-user ordering, flat across every dock/sidebar zone. Ids
319 + // not in the list keep their registration order and render
320 + // after the listed ones. Unknown ids are tolerated.
321 + 'navOrder' => array(),
322 + // Which experience the shell renders: 'auto' follows the
323 + // viewport, 'desktop' / 'mobile' force it. See `includes/mobile.php`.
324 + 'mobileLayout' => 'auto',
325 + // Ids pinned to the phone tab bar, at most three. Empty means
326 + // the server default (`openstation_mobile_tab_bar`).
327 + 'mobileTabs' => array(),
328 + // Persisted desktop position for every item the user has
329 + // promoted to the wallpaper via `navPlacement[id]=desktop|both`.
269 330 // Keyed by item id, value is `{ x: int, y: int }`. The JS
270 331 // synthesizer reads this when building a synthetic placement so
271 332 // the icon lands where the user last dragged it instead of
272 333 // resetting to (0, 0) on every reload. Capped at 256 entries.
@@ -285,14 +346,14 @@
285 346 */
286 347 function openstation_get_os_settings( $user_id ) {
287 348 $user_id = (int) $user_id;
288 349 if ( $user_id <= 0 ) {
289 - return openstation_default_os_settings();
350 + return openstation_sanitize_os_settings( array() );
290 351 }
291 352
292 353 $raw = get_user_meta( $user_id, OPENSTATION_OS_SETTINGS_META_KEY, true );
293 354 if ( ! is_array( $raw ) ) {
294 - return openstation_default_os_settings();
355 + return openstation_sanitize_os_settings( array() );
295 356 }
296 357
297 358 return openstation_sanitize_os_settings( $raw );
298 359 }
@@ -314,8 +375,76 @@
314 375 return false !== update_user_meta( $user_id, OPENSTATION_OS_SETTINGS_META_KEY, $clean );
315 376 }
316 377
317 378 /**
379 + * Strip the rail-synthesis prefix an id could carry before the
380 + * navigation model.
381 + *
382 + * `dock:<id>` / `desktop:<id>` used to mean "this tile is a copy of an
383 + * item whose real home is the other rail". Nothing synthesizes copies
384 + * any more — an item is one item wherever it is painted — so the
385 + * prefix is noise, and left in place it would key a preference to an
386 + * id nothing registers.
387 + *
388 + * @param string $id Possibly-prefixed id.
389 + * @return string Canonical id.
390 + */
391 +function openstation_canonical_nav_id( $id ) {
392 + $id = (string) $id;
393 + if ( 0 === strpos( $id, 'dock:' ) ) {
394 + return substr( $id, 5 );
395 + }
396 + if ( 0 === strpos( $id, 'desktop:' ) ) {
397 + return substr( $id, 8 );
398 + }
399 + return $id;
400 +}
401 +
402 +/**
403 + * Carry a pre-navigation `itemVisibility` map into `navPlacement`.
404 + *
405 + * The only value that moves is `'dock'` → `'rail'`: the stored name
406 + * is now the REGION rather than a rail, so a Core admin menu the user
407 + * kept on a rail follows the layout into the sidebar instead of
408 + * needing a second migration the first time they switch.
409 + *
410 + * Runs on read (see {@see openstation_sanitize_os_settings()}) rather
411 + * than as a numbered migration, because OS settings are per-user meta
412 + * and a site with many users would pay for a sweep that the next save
413 + * performs for free.
414 + *
415 + * @param array $visibility Legacy map of item id → placement.
416 + * @return array Map of canonical item id → nav placement.
417 + */
418 +function openstation_migrate_item_visibility( $visibility ) {
419 + $map = array(
420 + 'dock' => 'rail',
421 + 'desktop' => 'desktop',
422 + 'both' => 'both',
423 + 'hidden' => 'hidden',
424 + );
425 +
426 + $out = array();
427 + foreach ( (array) $visibility as $key => $val ) {
428 + if ( ! is_string( $key ) || ! is_string( $val ) || ! isset( $map[ $val ] ) ) {
429 + continue;
430 + }
431 + $id = openstation_canonical_nav_id( $key );
432 + if ( '' === $id ) {
433 + continue;
434 + }
435 + // A prefixed and an unprefixed key can collapse onto the same
436 + // id. The unprefixed one is the item's own preference rather
437 + // than a synthesized copy's, so it wins whichever order they
438 + // arrive in.
439 + if ( $id === $key || ! isset( $out[ $id ] ) ) {
440 + $out[ $id ] = $map[ $val ];
441 + }
442 + }
443 + return $out;
444 +}
445 +
446 +/**
318 447 * Sanitizes a raw OS settings payload.
319 448 *
320 449 * Unknown keys are ignored; known keys are coerced field-by-field so a
321 450 * partial save (e.g., only accent changed) merges cleanly with the
@@ -327,16 +456,22 @@
327 456 function openstation_sanitize_os_settings( $raw ) {
328 457 $defaults = openstation_default_os_settings();
329 458
330 459 if ( ! is_array( $raw ) ) {
331 - return $defaults;
460 + $raw = array();
332 461 }
333 462
334 463 // Wallpaper — any non-empty string; registry membership is validated
335 464 // client-side at apply time.
336 - $wallpaper = isset( $raw['wallpaper'] ) && is_string( $raw['wallpaper'] ) && '' !== $raw['wallpaper']
337 - ? sanitize_key( $raw['wallpaper'] )
338 - : $defaults['wallpaper'];
465 + // Key-clean per `/` segment: a desktop theme's wallpapers are
466 + // registered as `desktop-theme/<theme>/<id>`, and a flat
467 + // `sanitize_key()` would fold that into an id nothing answers to.
468 + $wallpaper = isset( $raw['wallpaper'] ) && is_string( $raw['wallpaper'] )
469 + ? implode( '/', array_filter( array_map( 'sanitize_key', explode( '/', $raw['wallpaper'] ) ) ) )
470 + : '';
471 + if ( '' === $wallpaper ) {
472 + $wallpaper = $defaults['wallpaper'];
473 + }
339 474
340 475 // Accent — non-empty string; swatch validity is enforced in the picker.
341 476 $accent = isset( $raw['accent'] ) && is_string( $raw['accent'] ) && '' !== $raw['accent']
342 477 ? sanitize_key( $raw['accent'] )
@@ -361,8 +496,13 @@
361 496 $window_radius = isset( $raw['windowRadius'] ) && in_array( $raw['windowRadius'], OPENSTATION_OS_SETTINGS_WINDOW_RADII, true )
362 497 ? (string) $raw['windowRadius']
363 498 : $defaults['windowRadius'];
364 499
500 + // How a newly opened window lands — one of the three known values.
501 + $open_windows_as = isset( $raw['openWindowsAs'] ) && in_array( $raw['openWindowsAs'], OPENSTATION_OS_SETTINGS_OPEN_WINDOWS_AS, true )
502 + ? (string) $raw['openWindowsAs']
503 + : $defaults['openWindowsAs'];
504 +
365 505 // Admin-bar mode — must be one of the three known values.
366 506 $admin_bar_mode = isset( $raw['adminBarMode'] )
367 507 && in_array( $raw['adminBarMode'], OPENSTATION_OS_SETTINGS_ADMIN_BAR_MODES, true )
368 508 ? (string) $raw['adminBarMode']
@@ -367,8 +507,19 @@
367 507 && in_array( $raw['adminBarMode'], OPENSTATION_OS_SETTINGS_ADMIN_BAR_MODES, true )
368 508 ? (string) $raw['adminBarMode']
369 509 : $defaults['adminBarMode'];
370 510
511 + // Dock behavior — must be one of the two known values. One answer
512 + // per rail: the dock, and the Split layout's sidebar.
513 + $dock_behavior = isset( $raw['dockBehavior'] )
514 + && in_array( $raw['dockBehavior'], OPENSTATION_OS_SETTINGS_DOCK_BEHAVIORS, true )
515 + ? (string) $raw['dockBehavior']
516 + : $defaults['dockBehavior'];
517 + $side_dock_behavior = isset( $raw['sideDockBehavior'] )
518 + && in_array( $raw['sideDockBehavior'], OPENSTATION_OS_SETTINGS_DOCK_BEHAVIORS, true )
519 + ? (string) $raw['sideDockBehavior']
520 + : $defaults['sideDockBehavior'];
521 +
371 522 // Desktop layout — must be one of the known values (`classic`,
372 523 // `unified`). Default `unified`.
373 524 $desktop_layout = isset( $raw['desktopLayout'] )
374 525 && in_array( $raw['desktopLayout'], OPENSTATION_OS_SETTINGS_DESKTOP_LAYOUTS, true )
@@ -649,8 +800,27 @@
649 800 // user meta indefinitely.
650 801 $native_posts_hidden_columns = array_slice( array_values( array_unique( $native_posts_hidden_columns ) ), 0, 32 );
651 802 }
652 803
804 + $native_pages_hidden_columns = $defaults['nativePagesHiddenColumns'];
805 + if ( isset( $raw['nativePagesHiddenColumns'] ) && is_array( $raw['nativePagesHiddenColumns'] ) ) {
806 + $native_pages_hidden_columns = array();
807 + foreach ( $raw['nativePagesHiddenColumns'] as $col ) {
808 + if ( ! is_string( $col ) || '' === $col ) {
809 + continue;
810 + }
811 + $slug = sanitize_key( $col );
812 + if ( '' === $slug ) {
813 + continue;
814 + }
815 + $native_pages_hidden_columns[] = $slug;
816 + }
817 + // Cap to a sane upper bound — far more than any plausible
818 + // column count, but blocks a malicious payload from bloating
819 + // user meta indefinitely.
820 + $native_pages_hidden_columns = array_slice( array_values( array_unique( $native_pages_hidden_columns ) ), 0, 32 );
821 + }
822 +
653 823 $native_pages_enabled = isset( $raw['nativePagesEnabled'] )
654 824 ? (bool) $raw['nativePagesEnabled']
655 825 : $defaults['nativePagesEnabled'];
656 826
@@ -665,15 +835,29 @@
665 835 $native_comments_enabled = isset( $raw['nativeCommentsEnabled'] )
666 836 ? (bool) $raw['nativeCommentsEnabled']
667 837 : $defaults['nativeCommentsEnabled'];
668 838
839 + $station_home_enabled = isset( $raw['stationHomeEnabled'] )
840 + ? (bool) $raw['stationHomeEnabled']
841 + : $defaults['stationHomeEnabled'];
842 +
843 + // Site-wide performance controls supersede the legacy per-user values.
844 + // Keep the snapshot keys so existing window consumers can read them.
845 + $extended_options = openstation_get_extended_options();
846 + $admin_asset_cache_enabled = $extended_options['admin_asset_cache'];
847 + $window_prewarm_enabled = $extended_options['window_prewarm'];
848 +
669 849 $show_desktop_on_wallpaper_click = isset( $raw['showDesktopOnWallpaperClick'] )
670 850 ? (bool) $raw['showDesktopOnWallpaperClick']
671 851 : $defaults['showDesktopOnWallpaperClick'];
672 852
853 + $confirm_close_all_windows = isset( $raw['confirmCloseAllWindows'] )
854 + ? (bool) $raw['confirmCloseAllWindows']
855 + : $defaults['confirmCloseAllWindows'];
856 +
673 857 $mio_enabled = isset( $raw['mioEnabled'] )
674 858 ? (bool) $raw['mioEnabled']
675 - : $defaults['mioEnabled'];
859 + : ( isset( $raw['mioApiEnabled'] ) ? (bool) $raw['mioApiEnabled'] : $defaults['mioEnabled'] );
676 860
677 861 // A missing key means "no look saved yet", which sanitizes to the
678 862 // same pair of empty arrays the defaults carry — so this needs no
679 863 // isset() branch of its own.
@@ -688,20 +872,59 @@
688 872 $developer_mode_enabled = isset( $raw['developerModeEnabled'] )
689 873 ? (bool) $raw['developerModeEnabled']
690 874 : $defaults['developerModeEnabled'];
691 875
876 + // mobileLayout — the phone/desktop override.
877 + $mobile_layout = isset( $raw['mobileLayout'] )
878 + && in_array( $raw['mobileLayout'], OPENSTATION_OS_SETTINGS_MOBILE_LAYOUTS, true )
879 + ? (string) $raw['mobileLayout']
880 + : $defaults['mobileLayout'];
881 +
882 + // mobileTabs — ordered nav ids pinned to the phone tab bar. Same
883 + // id grammar as navOrder, capped at the tab bar's slot count.
884 + $mobile_tabs = array();
885 + if ( isset( $raw['mobileTabs'] ) && is_array( $raw['mobileTabs'] ) ) {
886 + $seen_tabs = array();
887 + foreach ( $raw['mobileTabs'] as $id ) {
888 + if ( ! is_string( $id ) || '' === $id ) {
889 + continue;
890 + }
891 + $slug = sanitize_key( openstation_canonical_nav_id( $id ) );
892 + if ( '' === $slug || isset( $seen_tabs[ $slug ] ) ) {
893 + continue;
894 + }
895 + $seen_tabs[ $slug ] = true;
896 + $mobile_tabs[] = $slug;
897 + if ( count( $mobile_tabs ) >= OPENSTATION_OS_SETTINGS_MOBILE_TABS_MAX ) {
898 + break;
899 + }
900 + }
901 + }
902 +
692 903 $folders_sharing_enabled = isset( $raw['foldersSharingEnabled'] )
693 904 ? (bool) $raw['foldersSharingEnabled']
694 905 : $defaults['foldersSharingEnabled'];
695 906
696 - // itemVisibility — map<sanitize_key, enum>. Unknown ids are kept
907 + // navPlacement — map<sanitize_key, enum>. Unknown ids are kept
697 908 // (a deactivated plugin's setting should survive reactivation);
698 909 // invalid placement values are dropped.
699 - $item_visibility = array();
700 - if ( isset( $raw['itemVisibility'] ) && is_array( $raw['itemVisibility'] ) ) {
701 - $allowed_placements = array( 'both', 'dock', 'desktop', 'hidden' );
910 + //
911 + // Reads the pre-navigation `itemVisibility` map when this user has
912 + // no `navPlacement` yet, so an existing arrangement carries over on
913 + // first load and is written back on the next save. See
914 + // `openstation_migrate_item_visibility()`.
915 + $raw_placement = array();
916 + if ( isset( $raw['navPlacement'] ) && is_array( $raw['navPlacement'] ) ) {
917 + $raw_placement = $raw['navPlacement'];
918 + } elseif ( isset( $raw['itemVisibility'] ) && is_array( $raw['itemVisibility'] ) ) {
919 + $raw_placement = openstation_migrate_item_visibility( $raw['itemVisibility'] );
920 + }
921 +
922 + $nav_placement = array();
923 + if ( ! empty( $raw_placement ) ) {
924 + $allowed_placements = array( 'both', 'rail', 'desktop', 'hidden' );
702 925 $count = 0;
703 - foreach ( $raw['itemVisibility'] as $key => $val ) {
926 + foreach ( $raw_placement as $key => $val ) {
704 927 if ( $count >= 256 ) {
705 928 break;
706 929 }
707 930 if ( ! is_string( $key ) || '' === $key || ! is_string( $val ) ) {
@@ -713,34 +936,38 @@
713 936 }
714 937 if ( ! in_array( $val, $allowed_placements, true ) ) {
715 938 continue;
716 939 }
717 - $item_visibility[ $slug ] = $val;
940 + $nav_placement[ $slug ] = $val;
718 941 ++$count;
719 942 }
720 943 }
721 944
722 - // dockOrder — ordered list of item ids. Most are sanitize_key()-
723 - // clean dock slugs, but cross-rail tiles the user promoted carry a
724 - // rail-synthesis prefix (`desktop:<id>` / `dock:<id>`, built by
725 - // src/settings/item-placement.ts). sanitize_key() strips the colon,
726 - // which silently breaks the JS order match on reload and can collide
727 - // with an unrelated id — so allow the colon (and hyphen/underscore)
728 - // while still rejecting anything outside the JS id charset.
729 - $dock_order = array();
730 - if ( isset( $raw['dockOrder'] ) && is_array( $raw['dockOrder'] ) ) {
945 + // navOrder — ordered list of item ids, flat across every zone.
946 + // Reads the pre-navigation `dockOrder` when absent, stripping the
947 + // rail-synthesis prefixes (`dock:` / `desktop:`) that model no
948 + // longer has.
949 + $raw_order = array();
950 + if ( isset( $raw['navOrder'] ) && is_array( $raw['navOrder'] ) ) {
951 + $raw_order = $raw['navOrder'];
952 + } elseif ( isset( $raw['dockOrder'] ) && is_array( $raw['dockOrder'] ) ) {
953 + $raw_order = $raw['dockOrder'];
954 + }
955 +
956 + $nav_order = array();
957 + if ( ! empty( $raw_order ) ) {
731 958 $seen = array();
732 - foreach ( $raw['dockOrder'] as $id ) {
959 + foreach ( $raw_order as $id ) {
733 960 if ( ! is_string( $id ) || '' === $id ) {
734 961 continue;
735 962 }
736 - $slug = (string) preg_replace( '/[^a-z0-9_:-]+/', '', strtolower( $id ) );
963 + $slug = sanitize_key( openstation_canonical_nav_id( $id ) );
737 964 if ( '' === $slug || isset( $seen[ $slug ] ) ) {
738 965 continue;
739 966 }
740 967 $seen[ $slug ] = true;
741 - $dock_order[] = $slug;
742 - if ( count( $dock_order ) >= 256 ) {
968 + $nav_order[] = $slug;
969 + if ( count( $nav_order ) >= 256 ) {
743 970 break;
744 971 }
745 972 }
746 973 }
@@ -791,11 +1018,14 @@
791 1018 'accent' => $accent,
792 1019 'customAccent' => $custom_accent,
793 1020 'dockSize' => $dock_size,
794 1021 'windowRadius' => $window_radius,
1022 + 'openWindowsAs' => $open_windows_as,
795 1023 'adminBarMode' => $admin_bar_mode,
796 1024 'desktopLayout' => $desktop_layout,
797 1025 'dockPlacement' => $dock_placement,
1026 + 'dockBehavior' => $dock_behavior,
1027 + 'sideDockBehavior' => $side_dock_behavior,
798 1028 'dockRailRenderer' => $dock_rail_renderer,
799 1029 'desktopTheme' => $desktop_theme,
800 1030 'appliedThemeRecommendations' => $applied_theme_recommendations,
801 1031 'unfocusEffect' => $unfocus_effect,
@@ -814,19 +1044,28 @@
814 1044 'heartbeatRate' => $heartbeat_rate,
815 1045 'nativePostsEnabled' => $native_posts_enabled,
816 1046 'nativePostsHiddenColumns' => $native_posts_hidden_columns,
817 1047 'nativePagesEnabled' => $native_pages_enabled,
1048 + 'nativePagesHiddenColumns' => $native_pages_hidden_columns,
818 1049 'nativeUsersEnabled' => $native_users_enabled,
819 1050 'nativePluginsEnabled' => $native_plugins_enabled,
820 1051 'nativeCommentsEnabled' => $native_comments_enabled,
1052 + 'stationHomeEnabled' => $station_home_enabled,
1053 + 'adminAssetCacheEnabled' => $admin_asset_cache_enabled,
1054 + 'windowPrewarmEnabled' => $window_prewarm_enabled,
821 1055 'showDesktopOnWallpaperClick' => $show_desktop_on_wallpaper_click,
1056 + 'confirmCloseAllWindows' => $confirm_close_all_windows,
822 1057 'mioEnabled' => $mio_enabled,
1058 + 'mioApiEnabled' => $mio_enabled,
1059 + 'mioShowOnWallpaper' => isset( $raw['mioShowOnWallpaper'] ) ? (bool) $raw['mioShowOnWallpaper'] : $defaults['mioShowOnWallpaper'],
823 1060 'mioStyle' => $mio_style,
824 1061 'showPostStatusRibbons' => $show_post_status_ribbons,
825 1062 'developerModeEnabled' => $developer_mode_enabled,
826 1063 'foldersSharingEnabled' => $folders_sharing_enabled,
827 - 'itemVisibility' => $item_visibility,
828 - 'dockOrder' => $dock_order,
1064 + 'navPlacement' => $nav_placement,
1065 + 'navOrder' => $nav_order,
1066 + 'mobileLayout' => $mobile_layout,
1067 + 'mobileTabs' => $mobile_tabs,
829 1068 'dockPromotedPositions' => $dock_promoted_positions,
830 1069 );
831 1070 }
832 1071
@@ -902,9 +1141,9 @@
902 1141 * defaults. Give the saver merge semantics and that migration
903 1142 * silently becomes a no-op.
904 1143 *
905 1144 * Merging is shallow, one level deep. For the map-shaped fields
906 - * (`wallpaperSettings`, `itemVisibility`, `dockOrder`,
1145 + * (`wallpaperSettings`, `navPlacement`, `navOrder`,
907 1146 * `dockPromotedPositions`) a request that sends the key replaces the
908 1147 * whole map — deep-merging them would leave no way to delete an
909 1148 * entry.
910 1149 *
@@ -926,8 +1165,13 @@
926 1165 // true of every path into this handler, not just the ones the
927 1166 // schema happens to guard.
928 1167 if ( ! is_array( $payload ) ) {
929 1168 return rest_ensure_response( openstation_get_os_settings( $user_id ) );
1169 + }
1170 +
1171 + // Normalize an alias-only patch before merging it with the saved master value.
1172 + if ( ! array_key_exists( 'mioEnabled', $payload ) && array_key_exists( 'mioApiEnabled', $payload ) ) {
1173 + $payload['mioEnabled'] = $payload['mioApiEnabled'];
930 1174 }
931 1175
932 1176 openstation_save_os_settings(
933 1177 $user_id,