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/welcome-dialog.php +537 -445 0.9.11.1.10 View file →
@@ -1,30 +1,30 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — First-run welcome dialog.
3 + * OpenStation — First-run welcome dialog.
4 4 *
5 - * Renders a one-time, self-contained modal inside the *classic* WordPress
6 - * admin (never inside the desktop shell or a chromeless iframe) telling
7 - * the user where the "Switch to Desktop Mode" button lives in the admin
8 - * bar. Dismissal is persisted via the existing seen-intros registry
5 + * Renders a one-time, self-contained modal inside the *classic*
6 + * WordPress admin (never inside the desktop shell or a chromeless
7 + * iframe) that introduces OpenStation and offers to switch it on.
8 + * Dismissal is persisted via the existing seen-intros registry
9 9 * (`desktop_mode_seen_intros` user meta, slug `activation-welcome`),
10 - * which means the "Reset what's-new dialogs" button in OS Settings →
11 - * Features brings it back exactly like every other intro dialog.
10 + * which means the "Reset what's-new dialogs" button in OpenStation
11 + * Preferences → Features brings it back exactly like every other intro
12 + * dialog.
12 13 *
13 14 * The dialog is intentionally self-contained — all HTML, CSS and JS are
14 15 * inlined into `admin_footer`. We deliberately do NOT use any of the
15 - * `<wpd-*>` shell components here because they only ship inside the
16 + * `<os-*>` shell components here because they only ship inside the
16 17 * desktop bundle, which is precisely *not* loaded on the classic admin
17 18 * screens where this dialog is allowed to appear.
18 19 *
19 - * @package Desktop_Mode
20 - * @since 0.18.5
20 + * @package OpenStation
21 21 */
22 22
23 23 defined( 'ABSPATH' ) || exit;
24 24
25 25 /** Slug stored in `desktop_mode_seen_intros` for this dialog. */
26 -const DESKTOP_MODE_WELCOME_INTRO_SLUG = 'activation-welcome';
26 +const OPENSTATION_WELCOME_INTRO_SLUG = 'activation-welcome';
27 27
28 28 /**
29 29 * Decides whether the welcome dialog should render on the current request.
30 30 *
@@ -35,25 +35,24 @@
35 35 * no destructive surface, but anonymous output makes no sense).
36 36 * 3. The request is NOT chromeless — chromeless pages are iframes
37 37 * rendering inside the desktop shell; the parent shell already shows
38 38 * its own UX.
39 - * 4. Desktop Mode is NOT already enabled for the user. This is a
40 - * "switch to Desktop Mode" promo, so it has nothing to say once the
39 + * 4. OpenStation is NOT already enabled for the user. This is a
40 + * "switch to OpenStation" promo, so it has nothing to say once the
41 41 * user is in the shell. The desktop shell's *parent* page is admin
42 42 * context and is not chromeless, so without this gate the dialog
43 - * re-renders there the moment the user clicks "Enable it now" — read
44 - * as a duplicate dialog, because the fire-and-forget seen-intro POST
45 - * races the redirect into the shell and often loses.
43 + * re-renders there the moment the user clicks "Switch to
44 + * OpenStation", which reads as a duplicate dialog because the
45 + * fire-and-forget seen-intro POST races the redirect into the shell
46 + * and often loses.
46 47 * 5. The user has not already dismissed this intro.
47 - * 6. The `desktop_mode_show_welcome_dialog` filter returns truthy, so
48 + * 6. The `openstation_show_welcome_dialog` filter returns truthy, so
48 49 * sites can suppress the dialog entirely (e.g. managed-host onboarding
49 50 * flows that ship their own).
50 51 *
51 - * @since 0.18.5
52 - *
53 52 * @return bool
54 53 */
55 -function desktop_mode_should_show_welcome_dialog() {
54 +function openstation_should_show_welcome_dialog() {
56 55 if ( ! is_admin() || ! is_user_logged_in() ) {
57 56 return false;
58 57 }
59 58 if ( ! current_user_can( 'read' ) ) {
@@ -58,16 +57,16 @@
58 57 }
59 58 if ( ! current_user_can( 'read' ) ) {
60 59 return false;
61 60 }
62 - if ( function_exists( 'desktop_mode_is_chromeless_request' ) && desktop_mode_is_chromeless_request() ) {
61 + if ( function_exists( 'openstation_is_chromeless_request' ) && openstation_is_chromeless_request() ) {
63 62 return false;
64 63 }
65 - if ( function_exists( 'desktop_mode_is_enabled' ) && desktop_mode_is_enabled() ) {
64 + if ( function_exists( 'openstation_is_enabled' ) && openstation_is_enabled() ) {
66 65 return false;
67 66 }
68 67 $user_id = get_current_user_id();
69 - if ( desktop_mode_has_seen_intro( $user_id, DESKTOP_MODE_WELCOME_INTRO_SLUG ) ) {
68 + if ( openstation_has_seen_intro( $user_id, OPENSTATION_WELCOME_INTRO_SLUG ) ) {
70 69 return false;
71 70 }
72 71
73 72 /**
@@ -75,535 +74,585 @@
75 74 * the current user on the current request. All earlier gates
76 75 * (admin context, capability, chromeless, seen-state) have
77 76 * already passed by the time this filter fires.
78 77 *
79 - * @since 0.18.5
80 - *
81 78 * @param bool $show Whether to render the dialog. Default true.
82 79 * @param int $user_id Current user ID.
83 80 */
84 - return (bool) apply_filters( 'desktop_mode_show_welcome_dialog', true, $user_id );
81 + return (bool) apply_filters( 'openstation_show_welcome_dialog', true, $user_id );
85 82 }
86 83
87 84 /**
85 + * Returns one of OpenStation's own icons as inline SVG markup.
86 + *
87 + * Reads the outlined copies in `assets/icons/` (the ones registered with
88 + * Core's icon registry), which paint with `currentColor`, so the dialog's
89 + * CSS decides their colour. Returns an empty string for a missing file,
90 + * which leaves an empty icon slot rather than breaking the dialog.
91 + *
92 + * @param string $slug Icon slug, e.g. `windows`.
93 + * @return string Sanitised SVG markup.
94 + */
95 +function openstation_welcome_dialog_icon( $slug ) {
96 + $path = OPENSTATION_DIR . 'assets/icons/' . sanitize_key( $slug ) . '.svg';
97 + if ( ! is_readable( $path ) ) {
98 + return '';
99 + }
100 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin file, not a remote URL.
101 + $svg = (string) file_get_contents( $path );
102 +
103 + return wp_kses(
104 + $svg,
105 + array(
106 + 'svg' => array(
107 + 'xmlns' => true,
108 + 'viewbox' => true,
109 + 'width' => true,
110 + 'height' => true,
111 + 'aria-hidden' => true,
112 + 'focusable' => true,
113 + ),
114 + 'path' => array(
115 + 'd' => true,
116 + 'fill' => true,
117 + ),
118 + )
119 + );
120 +}
121 +
122 +/**
88 123 * Prints the welcome dialog markup, styles and dismiss script into
89 124 * `admin_footer`.
90 125 *
91 126 * Self-contained on purpose: everything is scoped under the
92 - * `.desktop-mode-welcome` namespace so it cannot collide with the host
127 + * `.os-welcome` namespace so it cannot collide with the host
93 128 * admin theme. The dismiss button POSTs to the seen-intros REST route,
94 129 * which is exactly the same endpoint the in-shell intros use.
95 130 *
96 - * @since 0.18.5
131 + * The look is deliberately quiet: a light card with a dark panel that
132 + * shows two OpenStation windows, the holo mark, and Pulse kept to the
133 + * feature icons. Every colour is a literal from the brand palette rather
134 + * than a `--os-*` token, because `variables.css` is not loaded in classic
135 + * admin. Spacing sits on an 8px grid.
97 136 */
98 -function desktop_mode_render_welcome_dialog() {
99 - if ( ! desktop_mode_should_show_welcome_dialog() ) {
137 +function openstation_render_welcome_dialog() {
138 + if ( ! openstation_should_show_welcome_dialog() ) {
100 139 return;
101 140 }
102 141
103 - $rest_url = esc_url_raw( rest_url( 'desktop-mode/v1/intros/seen' ) );
104 - $rest_nonce = wp_create_nonce( 'wp_rest' );
105 - $ajax_url = esc_url_raw( admin_url( 'admin-ajax.php' ) );
106 - $ajax_nonce = wp_create_nonce( 'save-desktop-mode' );
107 - $slug = DESKTOP_MODE_WELCOME_INTRO_SLUG;
142 + $rest_url = esc_url_raw( rest_url( 'desktop-mode/v1/intros/seen' ) );
143 + $rest_nonce = wp_create_nonce( 'wp_rest' );
144 + $ajax_url = esc_url_raw( admin_url( 'admin-ajax.php' ) );
145 + $ajax_nonce = wp_create_nonce( 'save-openstation' );
146 + $slug = OPENSTATION_WELCOME_INTRO_SLUG;
147 + $font_url = OPENSTATION_URL . 'assets/fonts/Geist-Variable.woff2';
148 + $mono_url = OPENSTATION_URL . 'assets/fonts/GeistMono-Variable.woff2';
149 + $mark_url = OPENSTATION_URL . 'assets/images/openstation-mark-holo.svg';
108 150
109 151 // All user-facing strings are passed through translation; the dialog
110 - // is keyboard-dismissible (Escape) and focus-trapped between the
111 - // close button and the "Got it" CTA.
112 - $title = __( 'Welcome to Desktop Mode', 'desktop-mode' );
113 - $subtitle = __( 'A floating, window-based workspace for the WordPress admin — more like a real OS, less like a webpage.', 'desktop-mode' );
114 - $body = __( 'Desktop Mode reimagines the WordPress admin as a true desktop environment. Open multiple admin screens side-by-side, drag and drop content between windows, and keep your work in view as you move between tasks.', 'desktop-mode' );
115 - $cta = __( 'Got it', 'desktop-mode' );
116 - $enable = __( 'Enable it now', 'desktop-mode' );
117 - $enabling = __( 'Enabling…', 'desktop-mode' );
118 - $close_a = __( 'Close', 'desktop-mode' );
152 + // is keyboard-dismissible (Escape) and moves initial focus to the
153 + // primary CTA.
154 + $title = __( 'Welcome to OpenStation', 'desktop-mode' );
155 + $body = __( 'Your admin, as a desktop. Keep several screens open at once and move between tasks without losing your place.', 'desktop-mode' );
156 + $later = __( 'Not now', 'desktop-mode' );
157 + $enable = __( 'Switch to OpenStation', 'desktop-mode' );
158 + $enabling = __( 'Switching…', 'desktop-mode' );
119 159
120 160 $features = array(
121 161 array(
122 - 'icon' => '🪟',
123 - 'title' => __( 'Multi-window workspace', 'desktop-mode' ),
124 - 'desc' => __( 'Open Posts, Media and Plugins in their own draggable, resizable windows. Tile, cascade or stack them however you work best.', 'desktop-mode' ),
162 + 'icon' => 'windows',
163 + 'title' => __( 'Work in windows', 'desktop-mode' ),
164 + 'desc' => __( 'Posts, media and settings side by side.', 'desktop-mode' ),
125 165 ),
126 166 array(
127 - 'icon' => '⚡',
128 - 'title' => __( 'Native, table-driven apps', 'desktop-mode' ),
129 - 'desc' => __( 'Posts, Pages, Users and Plugins are reimplemented as fast native windows with sticky headers, bulk actions, multi-select and inline previews.', 'desktop-mode' ),
167 + 'icon' => 'apps',
168 + 'title' => __( 'Apps for everyday tasks', 'desktop-mode' ),
169 + 'desc' => __( 'Fast lists with bulk actions and previews.', 'desktop-mode' ),
130 170 ),
131 171 array(
132 - 'icon' => '🎨',
133 - 'title' => __( 'Wallpapers, dock & themes', 'desktop-mode' ),
134 - 'desc' => __( 'Make it yours. Choose a wallpaper, pin your favorite screens to the dock, and switch accent colors — all from OS Settings.', 'desktop-mode' ),
172 + 'icon' => 'dock',
173 + 'title' => __( 'A dock for your screens', 'desktop-mode' ),
174 + 'desc' => __( 'Pin what you use most, one click away.', 'desktop-mode' ),
135 175 ),
136 176 array(
137 - 'icon' => '🤖',
138 - 'title' => __( 'AI Copilot, built in', 'desktop-mode' ),
139 - 'desc' => __( 'Press ⌘K anywhere to ask the AI Assistant — it can run commands, navigate windows and answer questions about your site.', 'desktop-mode' ),
177 + 'icon' => 'command',
178 + /* translators: %s: the keyboard shortcut that opens search, e.g. ⌘K. */
179 + 'title' => __( 'Search with %s', 'desktop-mode' ),
180 + 'desc' => __( 'Jump to any screen or run a command.', 'desktop-mode' ),
181 + 'kbd' => true,
140 182 ),
141 183 );
142 184 ?>
143 -<style id="desktop-mode-welcome-style">
144 - .desktop-mode-welcome,
145 - .desktop-mode-welcome * {
185 +<style id="os-welcome-style">
186 + @font-face {
187 + font-family: "OpenStation Geist";
188 + src: url( "<?php echo esc_url( $font_url ); ?>" ) format( "woff2" );
189 + font-weight: 100 900;
190 + font-style: normal;
191 + font-display: swap;
192 + }
193 + @font-face {
194 + font-family: "OpenStation Geist Mono";
195 + src: url( "<?php echo esc_url( $mono_url ); ?>" ) format( "woff2" );
196 + font-weight: 100 900;
197 + font-style: normal;
198 + font-display: swap;
199 + }
200 + .os-welcome,
201 + .os-welcome * {
146 202 box-sizing: border-box;
147 203 }
148 - .desktop-mode-welcome {
204 + .os-welcome {
205 + /* Brand palette, as literals: see the render function's docblock. */
206 + --_void: #0c0b0f;
207 + --_obsidian: #1a1721;
208 + --_astro: #33303a;
209 + --_silver: #4d4a52;
210 + --_pewter: #66636b;
211 + --_osmium: #99969c;
212 + --_ash: #b3afb5;
213 + --_cloud: #ccc8ce;
214 + --_mist: #e6e2e6;
215 + --_haze: #f4f1f4;
216 + --_starlight: #fffbff;
217 + --_pulse: #f252fc;
218 +
149 219 position: fixed;
150 220 inset: 0;
151 221 z-index: 100000;
152 222 display: flex;
153 - align-items: center;
154 - justify-content: center;
155 223 padding: 24px;
156 - background: radial-gradient(
157 - ellipse at 30% 20%,
158 - rgba( 56, 189, 248, 0.32 ),
159 - transparent 60%
160 - ),
161 - radial-gradient(
162 - ellipse at 70% 80%,
163 - rgba( 16, 185, 129, 0.26 ),
164 - transparent 55%
165 - ),
166 - rgba( 8, 14, 26, 0.62 );
167 - backdrop-filter: blur( 14px ) saturate( 140% );
168 - -webkit-backdrop-filter: blur( 14px ) saturate( 140% );
169 - animation: desktop-mode-welcome-fade 360ms cubic-bezier( 0.22, 1, 0.36, 1 );
170 - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
171 - "Helvetica Neue", Arial, sans-serif;
224 + overflow-y: auto;
225 + background: rgba( 12, 11, 15, 0.72 );
226 + backdrop-filter: blur( 16px );
227 + -webkit-backdrop-filter: blur( 16px );
228 + animation: os-welcome-fade 240ms ease-out;
229 + font-family: "OpenStation Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
230 + font-size: 16px;
231 + line-height: 1.5;
232 + color: var( --_obsidian );
172 233 -webkit-font-smoothing: antialiased;
173 234 -moz-osx-font-smoothing: grayscale;
174 235 }
175 - @keyframes desktop-mode-welcome-fade {
236 + @keyframes os-welcome-fade {
176 237 from { opacity: 0; }
177 238 to { opacity: 1; }
178 239 }
179 - @keyframes desktop-mode-welcome-pop {
180 - 0% { opacity: 0; transform: translateY( 14px ) scale( 0.96 ); }
181 - 100% { opacity: 1; transform: translateY( 0 ) scale( 1 ); }
240 + @keyframes os-welcome-pop {
241 + from { opacity: 0; transform: translateY( 8px ); }
242 + to { opacity: 1; transform: translateY( 0 ); }
182 243 }
183 - @keyframes desktop-mode-welcome-arrow {
184 - 0%, 100% { transform: translateX( 0 ); opacity: 0.85; }
185 - 50% { transform: translateX( 6px ); opacity: 1; }
186 - }
187 - @keyframes desktop-mode-welcome-shimmer {
188 - 0% { background-position: 0% 50%; }
189 - 100% { background-position: 200% 50%; }
190 - }
191 - .desktop-mode-welcome__card {
192 - position: relative;
244 + .os-welcome__card {
245 + /* margin:auto centres the card and still lets a short viewport scroll to its top. */
246 + margin: auto;
193 247 width: 100%;
194 - max-width: 760px;
195 - border-radius: 22px;
248 + max-width: 820px;
249 + min-height: 544px;
250 + display: grid;
251 + grid-template-columns: 5fr 6fr;
196 252 overflow: hidden;
197 - background: linear-gradient(
198 - 145deg,
199 - rgba( 255, 255, 255, 0.98 ) 0%,
200 - rgba( 244, 250, 253, 0.98 ) 100%
201 - );
253 + background: var( --_starlight );
254 + border-radius: 13px;
202 255 box-shadow:
203 - 0 1px 0 rgba( 255, 255, 255, 0.85 ) inset,
204 - 0 30px 60px -20px rgba( 8, 47, 73, 0.55 ),
205 - 0 18px 36px -18px rgba( 14, 165, 233, 0.45 );
206 - animation: desktop-mode-welcome-pop 460ms cubic-bezier( 0.22, 1, 0.36, 1 ) both;
207 - animation-delay: 60ms;
256 + 0 0 0 1px rgba( 12, 11, 15, 0.08 ),
257 + 0 32px 64px -24px rgba( 12, 11, 15, 0.48 );
258 + animation: os-welcome-pop 320ms cubic-bezier( 0.22, 1, 0.36, 1 ) both;
208 259 }
209 - .desktop-mode-welcome__card::before {
210 - content: "";
211 - position: absolute;
212 - inset: -1px;
213 - border-radius: inherit;
214 - padding: 1px;
215 - background: linear-gradient(
216 - 135deg,
217 - rgba( 14, 165, 233, 0.65 ),
218 - rgba( 6, 182, 212, 0.55 ),
219 - rgba( 16, 185, 129, 0.55 )
220 - );
221 - -webkit-mask: linear-gradient( #000 0 0 ) content-box,
222 - linear-gradient( #000 0 0 );
223 - -webkit-mask-composite: xor;
224 - mask-composite: exclude;
225 - pointer-events: none;
226 - }
227 - .desktop-mode-welcome__hero {
260 +
261 + /* ---- Dark panel: two windows on the station ------------------- */
262 +
263 + .os-welcome__art {
228 264 position: relative;
229 - padding: 36px 36px 24px;
230 - background: linear-gradient(
231 - 135deg,
232 - #0f172a 0%,
233 - #0e7490 45%,
234 - #06b6d4 100%
235 - );
236 - background-size: 200% 200%;
237 - animation: desktop-mode-welcome-shimmer 14s ease infinite alternate;
238 - color: #fff;
239 265 overflow: hidden;
266 + isolation: isolate;
267 + background:
268 + radial-gradient( 1px 1px at 12% 18%, rgba( 255, 251, 255, 0.55 ) 50%, transparent 51% ),
269 + radial-gradient( 1px 1px at 78% 12%, rgba( 255, 251, 255, 0.4 ) 50%, transparent 51% ),
270 + radial-gradient( 1px 1px at 64% 44%, rgba( 255, 251, 255, 0.3 ) 50%, transparent 51% ),
271 + radial-gradient( 1.5px 1.5px at 88% 62%, rgba( 255, 251, 255, 0.45 ) 50%, transparent 51% ),
272 + radial-gradient( 1px 1px at 22% 72%, rgba( 255, 251, 255, 0.35 ) 50%, transparent 51% ),
273 + radial-gradient( 1px 1px at 46% 88%, rgba( 255, 251, 255, 0.3 ) 50%, transparent 51% ),
274 + radial-gradient( 1px 1px at 8% 48%, rgba( 255, 251, 255, 0.3 ) 50%, transparent 51% ),
275 + radial-gradient( 1px 1px at 94% 90%, rgba( 255, 251, 255, 0.35 ) 50%, transparent 51% ),
276 + radial-gradient( 1px 1px at 36% 8%, rgba( 255, 251, 255, 0.35 ) 50%, transparent 51% ),
277 + radial-gradient( 60% 45% at 85% 100%, rgba( 236, 155, 255, 0.1 ), transparent 70% ),
278 + radial-gradient( 50% 40% at 0% 30%, rgba( 159, 152, 255, 0.07 ), transparent 70% ),
279 + var( --_void );
240 280 }
241 - .desktop-mode-welcome__hero::after {
281 + .os-welcome__art::after {
242 282 content: "";
243 283 position: absolute;
244 284 inset: 0;
285 + pointer-events: none;
245 286 background:
246 - radial-gradient( circle at 85% 15%, rgba( 56, 189, 248, 0.35 ), transparent 45% ),
247 - radial-gradient( circle at 15% 90%, rgba( 16, 185, 129, 0.22 ), transparent 50% ),
248 - linear-gradient( rgba( 255, 255, 255, 0.05 ) 1px, transparent 1px ) 0 0 / 28px 28px,
249 - linear-gradient( 90deg, rgba( 255, 255, 255, 0.05 ) 1px, transparent 1px ) 0 0 / 28px 28px;
250 - pointer-events: none;
251 - mask-image: radial-gradient( ellipse at 50% 40%, #000 35%, transparent 80% );
252 - -webkit-mask-image: radial-gradient( ellipse at 50% 40%, #000 35%, transparent 80% );
287 + linear-gradient( 180deg, rgba( 12, 11, 15, 0.85 ) 0%, rgba( 12, 11, 15, 0 ) 22% ),
288 + linear-gradient( 0deg, rgba( 12, 11, 15, 0.6 ) 0%, rgba( 12, 11, 15, 0 ) 26% );
253 289 }
254 - .desktop-mode-welcome__eyebrow {
255 - display: inline-flex;
290 + .os-welcome .os-welcome__mark {
291 + position: absolute;
292 + top: 32px;
293 + inset-inline-start: 32px;
294 + z-index: 2;
295 + display: block;
296 + width: 40px;
297 + height: 40px;
298 + max-width: none;
299 + }
300 + .os-welcome__pair {
301 + position: absolute;
302 + left: 50%;
303 + top: 50%;
304 + width: 300px;
305 + height: 276px;
306 + transform: translate( -50%, -44% );
307 + direction: ltr;
308 + }
309 + .os-welcome__win {
310 + position: absolute;
311 + overflow: hidden;
312 + border-radius: 9px;
313 + /* The window greys sit a step above the palette's dark ramp so the art reads against Void. */
314 + background: #201d28;
315 + border: 1px solid rgba( 255, 251, 255, 0.12 );
316 + box-shadow: 0 18px 36px -12px rgba( 0, 0, 0, 0.7 );
317 + font-size: 9px;
318 + line-height: 1;
319 + }
320 + .os-welcome__win--posts { left: 0; top: 0; width: 236px; height: 184px; }
321 + .os-welcome__win--media { right: 0; bottom: 0; width: 236px; height: 160px; }
322 + .os-welcome__bar {
323 + display: flex;
256 324 align-items: center;
325 + justify-content: space-between;
326 + height: 24px;
327 + padding: 0 8px;
328 + font-weight: 500;
329 + color: var( --_mist );
330 + border-bottom: 1px solid rgba( 255, 251, 255, 0.08 );
331 + }
332 + .os-welcome__bar-title { display: flex; align-items: center; gap: 8px; }
333 + .os-welcome__bar-title i { width: 7px; height: 7px; border: 1px solid var( --_osmium ); border-radius: 50%; }
334 + .os-welcome__bar-ctl { display: flex; gap: 8px; }
335 + .os-welcome__bar-ctl i { display: block; width: 6px; height: 6px; border: 1px solid #77747c; border-radius: 1.5px; }
336 + .os-welcome__bar-ctl i:first-child { height: 0; border-width: 1px 0 0; margin-top: 3px; border-radius: 0; }
337 + .os-welcome__rows { padding: 8px; }
338 + .os-welcome__row {
339 + display: grid;
340 + grid-template-columns: 7px 1fr 34px;
341 + align-items: center;
257 342 gap: 8px;
258 - padding: 5px 12px;
259 - font-size: 11px;
260 - font-weight: 600;
261 - letter-spacing: 0.12em;
262 - text-transform: uppercase;
263 - color: #fff;
264 - background: rgba( 255, 255, 255, 0.18 );
265 - border: 1px solid rgba( 255, 255, 255, 0.28 );
266 - border-radius: 999px;
267 - backdrop-filter: blur( 6px );
268 - -webkit-backdrop-filter: blur( 6px );
343 + height: 24px;
344 + border-bottom: 1px solid rgba( 255, 251, 255, 0.07 );
269 345 }
270 - .desktop-mode-welcome__eyebrow-dot {
271 - width: 6px;
272 - height: 6px;
273 - border-radius: 50%;
274 - background: #22d3ee;
275 - box-shadow: 0 0 0 4px rgba( 34, 211, 238, 0.28 );
346 + .os-welcome__row i { width: 7px; height: 7px; border: 1px solid #5c5963; border-radius: 2px; }
347 + .os-welcome__row b { height: 5px; border-radius: 3px; background: #5c5963; }
348 + .os-welcome__row em { height: 9px; border-radius: 999px; background: #3e3b46; }
349 + .os-welcome__row--head b,
350 + .os-welcome__row--head em { background: #3e3b46; }
351 + .os-welcome__row--selected { background: rgba( 255, 251, 255, 0.05 ); }
352 + .os-welcome__thumbs { padding: 8px; display: grid; grid-template-columns: repeat( 4, 1fr ); gap: 8px; }
353 + .os-welcome__thumbs i {
354 + display: block;
355 + aspect-ratio: 1;
356 + border-radius: 4px;
357 + background: linear-gradient( 150deg, #34303d, #27242f );
358 + border: 1px solid rgba( 255, 251, 255, 0.06 );
276 359 }
277 - .desktop-mode-welcome__title {
278 - margin: 14px 0 6px;
279 - font-size: 26px;
280 - line-height: 1.2;
281 - font-weight: 700;
360 + .os-welcome__thumbs i:nth-child( 3n + 1 ) { background: linear-gradient( 150deg, #3a3444, #2a2633 ); }
361 + .os-welcome__thumbs i:nth-child( 5 ) {
362 + background:
363 + radial-gradient( circle at 70% 30%, rgba( 236, 155, 255, 0.22 ), transparent 60% ),
364 + linear-gradient( 150deg, #36313f, #25222c );
365 + }
366 +
367 + /* ---- Content column ------------------------------------------- */
368 +
369 + .os-welcome__main {
370 + display: flex;
371 + flex-direction: column;
372 + min-width: 0;
373 + text-align: start;
374 + }
375 + .os-welcome__content {
376 + display: grid;
377 + gap: 24px;
378 + padding: 32px;
379 + }
380 + .os-welcome__head {
381 + display: grid;
382 + gap: 8px;
383 + }
384 + .os-welcome .os-welcome__title {
385 + margin: 0;
386 + padding: 0;
387 + font-family: inherit;
388 + font-size: 24px;
389 + line-height: 1.3;
390 + font-weight: 500;
282 391 letter-spacing: -0.01em;
283 - color: #fff;
392 + color: var( --_obsidian );
393 + text-wrap: balance;
284 394 }
285 - .desktop-mode-welcome__subtitle {
395 + .os-welcome .os-welcome__lede {
286 396 margin: 0;
287 - font-size: 14px;
397 + font-size: 16px;
288 398 line-height: 1.5;
289 - color: rgba( 255, 255, 255, 0.85 );
399 + color: var( --_silver );
290 400 }
291 - .desktop-mode-welcome__body {
292 - padding: 24px 36px 24px;
293 - font-size: 14.5px;
294 - line-height: 1.6;
295 - color: #1f2937;
296 - }
297 - .desktop-mode-welcome__lede {
401 + .os-welcome .os-welcome__features {
402 + display: grid;
403 + gap: 16px;
298 404 margin: 0;
299 - font-size: 14.5px;
300 - color: #374151;
301 - }
302 - .desktop-mode-welcome__features {
303 - display: grid;
304 - grid-template-columns: repeat( 2, minmax( 0, 1fr ) );
305 - gap: 14px;
306 - margin: 20px 0 0;
307 405 padding: 0;
308 406 list-style: none;
309 407 }
310 - .desktop-mode-welcome__feature {
311 - position: relative;
312 - padding: 14px 14px 14px 52px;
313 - background: linear-gradient(
314 - 145deg,
315 - rgba( 255, 255, 255, 0.85 ),
316 - rgba( 240, 249, 255, 0.85 )
317 - );
318 - border: 1px solid rgba( 14, 165, 233, 0.18 );
319 - border-radius: 12px;
320 - box-shadow: 0 1px 0 rgba( 255, 255, 255, 0.9 ) inset,
321 - 0 2px 6px -2px rgba( 8, 47, 73, 0.08 );
408 + .os-welcome .os-welcome__feature {
409 + display: flex;
410 + gap: 16px;
411 + margin: 0;
322 412 }
323 - .desktop-mode-welcome__feature-icon {
324 - position: absolute;
325 - top: 12px;
326 - left: 12px;
327 - width: 30px;
328 - height: 30px;
329 - display: inline-flex;
330 - align-items: center;
331 - justify-content: center;
332 - border-radius: 9px;
333 - background: linear-gradient( 135deg, rgba( 14, 165, 233, 0.16 ), rgba( 16, 185, 129, 0.16 ) );
334 - border: 1px solid rgba( 14, 165, 233, 0.22 );
335 - font-size: 16px;
336 - line-height: 1;
413 + .os-welcome__icon {
414 + flex: none;
415 + width: 20px;
416 + height: 20px;
417 + color: var( --_pulse );
337 418 }
338 - .desktop-mode-welcome__feature-title {
419 + .os-welcome__icon svg {
339 420 display: block;
340 - margin: 0 0 2px;
341 - font-size: 13.5px;
342 - font-weight: 700;
343 - color: #0f172a;
421 + width: 20px;
422 + height: 20px;
344 423 }
345 - .desktop-mode-welcome__feature-desc {
424 + .os-welcome__feature-title {
425 + display: block;
426 + font-size: 14px;
427 + font-weight: 600;
428 + line-height: 1.4;
429 + color: var( --_obsidian );
430 + }
431 + .os-welcome .os-welcome__feature-desc {
346 432 margin: 0;
347 - font-size: 12.5px;
433 + font-size: 14px;
348 434 line-height: 1.5;
349 - color: #475569;
435 + color: var( --_pewter );
350 436 }
351 - .desktop-mode-welcome__hint {
352 - display: flex;
353 - align-items: center;
354 - gap: 12px;
355 - margin: 18px 0 0;
356 - padding: 14px 16px;
357 - background: linear-gradient(
358 - 135deg,
359 - rgba( 14, 165, 233, 0.08 ),
360 - rgba( 16, 185, 129, 0.08 )
361 - );
362 - border: 1px solid rgba( 14, 165, 233, 0.22 );
363 - border-radius: 12px;
364 - font-size: 13.5px;
365 - color: #0c4a6e;
366 - }
367 - .desktop-mode-welcome__hint-icon {
368 - flex-shrink: 0;
369 - width: 24px;
370 - height: 24px;
371 - border-radius: 50%;
372 - display: inline-flex;
373 - align-items: center;
374 - justify-content: center;
375 - background: linear-gradient( 135deg, #0ea5e9, #06b6d4 );
376 - color: #fff;
377 - font-size: 14px;
378 - line-height: 1;
379 - animation: desktop-mode-welcome-arrow 1.8s ease-in-out infinite;
380 - }
381 - .desktop-mode-welcome__hint-icon::before {
382 - content: "→";
383 - font-weight: 700;
384 - }
385 - .desktop-mode-welcome__hint code {
437 + .os-welcome .os-welcome__kbd {
386 438 display: inline-block;
387 - padding: 2px 6px;
388 - margin: 0 2px;
389 - background: rgba( 255, 255, 255, 0.85 );
390 - border: 1px solid rgba( 14, 165, 233, 0.28 );
439 + margin: 0;
440 + padding: 0 8px;
441 + font-family: "OpenStation Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
442 + font-size: 12px;
443 + font-weight: 500;
444 + line-height: 16px;
445 + color: var( --_astro );
446 + background: var( --_haze );
447 + border: 1px solid var( --_mist );
448 + border-bottom-color: var( --_cloud );
391 449 border-radius: 5px;
392 - font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
393 - font-size: 12px;
394 - color: #0369a1;
450 + white-space: nowrap;
395 451 }
396 - .desktop-mode-welcome__actions {
452 +
453 + /* Anchored to the bottom of the card, however long the copy runs. */
454 + .os-welcome__actions {
455 + margin-top: auto;
397 456 display: flex;
398 - align-items: center;
457 + flex-wrap: wrap;
399 458 justify-content: flex-end;
400 - gap: 10px;
401 - padding: 0 36px 28px;
459 + gap: 8px;
460 + padding: 0 32px 32px;
402 461 }
403 - .desktop-mode-welcome__btn {
462 + .os-welcome .os-welcome__btn {
404 463 display: inline-flex;
405 464 align-items: center;
406 465 justify-content: center;
407 - gap: 6px;
408 - min-height: 38px;
409 - padding: 8px 18px;
466 + min-height: 40px;
467 + margin: 0;
468 + padding: 0 16px;
469 + font-family: inherit;
410 470 font-size: 14px;
411 - font-weight: 600;
412 - font-family: inherit;
471 + font-weight: 500;
413 472 line-height: 1;
414 - border-radius: 10px;
415 - border: 1px solid transparent;
473 + border-radius: 8px;
474 + border: 1px solid var( --_obsidian );
475 + box-shadow: none;
416 476 cursor: pointer;
417 - transition: transform 120ms ease, box-shadow 120ms ease,
418 - background 120ms ease, color 120ms ease;
477 + transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
419 478 }
420 - .desktop-mode-welcome__btn:focus-visible {
421 - outline: 2px solid #0ea5e9;
479 + .os-welcome .os-welcome__btn:focus-visible {
480 + outline: 2px solid var( --_obsidian );
422 481 outline-offset: 2px;
423 482 }
424 - .desktop-mode-welcome__btn--ghost {
425 - background: transparent;
426 - color: #475569;
427 - border-color: transparent;
483 + .os-welcome .os-welcome__btn--primary {
484 + color: var( --_starlight );
485 + background: var( --_obsidian );
428 486 }
429 - .desktop-mode-welcome__btn--ghost:hover {
430 - background: rgba( 15, 23, 42, 0.06 );
431 - color: #0f172a;
487 + .os-welcome .os-welcome__btn--primary:hover {
488 + background: var( --_astro );
489 + border-color: var( --_astro );
432 490 }
433 - .desktop-mode-welcome__btn--secondary {
434 - color: #0369a1;
435 - background: rgba( 14, 165, 233, 0.10 );
436 - border-color: rgba( 14, 165, 233, 0.28 );
491 + .os-welcome .os-welcome__btn--secondary {
492 + color: var( --_obsidian );
493 + background: transparent;
437 494 }
438 - .desktop-mode-welcome__btn--secondary:hover {
439 - background: rgba( 14, 165, 233, 0.16 );
440 - color: #075985;
495 + .os-welcome .os-welcome__btn--secondary:hover {
496 + background: var( --_haze );
441 497 }
442 - .desktop-mode-welcome__btn--primary {
443 - color: #fff;
444 - background: linear-gradient( 135deg, #0369a1, #0ea5e9 55%, #06b6d4 );
445 - background-size: 180% 180%;
446 - box-shadow: 0 10px 24px -10px rgba( 14, 165, 233, 0.75 ),
447 - 0 4px 10px -4px rgba( 6, 182, 212, 0.55 );
448 - }
449 - .desktop-mode-welcome__btn--primary:hover {
450 - transform: translateY( -1px );
451 - background-position: 100% 50%;
452 - box-shadow: 0 14px 30px -12px rgba( 14, 165, 233, 0.85 ),
453 - 0 6px 14px -4px rgba( 6, 182, 212, 0.65 );
454 - }
455 - .desktop-mode-welcome__btn--primary:active {
456 - transform: translateY( 0 );
457 - }
458 - .desktop-mode-welcome__btn[disabled] {
459 - opacity: 0.65;
498 + .os-welcome .os-welcome__btn[disabled] {
499 + opacity: 0.64;
460 500 cursor: progress;
461 501 }
462 - .desktop-mode-welcome__close {
463 - position: absolute;
464 - top: 14px;
465 - right: 14px;
466 - width: 32px;
467 - height: 32px;
468 - display: inline-flex;
469 - align-items: center;
470 - justify-content: center;
471 - padding: 0;
472 - background: rgba( 255, 255, 255, 0.18 );
473 - color: #fff;
474 - border: 1px solid rgba( 255, 255, 255, 0.25 );
475 - border-radius: 50%;
476 - cursor: pointer;
477 - font-size: 18px;
478 - line-height: 1;
479 - transition: background 120ms ease, transform 120ms ease;
480 - }
481 - .desktop-mode-welcome__close:hover {
482 - background: rgba( 255, 255, 255, 0.32 );
483 - transform: rotate( 90deg );
484 - }
485 - .desktop-mode-welcome__close:focus-visible {
486 - outline: 2px solid #fff;
487 - outline-offset: 2px;
488 - }
489 - body.desktop-mode-welcome-open {
502 + body.os-welcome-open {
490 503 overflow: hidden;
491 504 }
505 +
506 + /* ---- Narrow screens ------------------------------------------- */
507 +
492 508 @media ( max-width: 760px ) {
493 - .desktop-mode-welcome__features {
509 + .os-welcome__card {
494 510 grid-template-columns: 1fr;
511 + min-height: 0;
495 512 }
513 + .os-welcome__art {
514 + height: 216px;
515 + }
516 + .os-welcome__pair {
517 + transform: translate( -50%, -44% ) scale( 0.64 );
518 + }
496 519 }
497 - @media ( max-width: 600px ) {
498 - .desktop-mode-welcome__hero {
499 - padding: 28px 24px 20px;
520 + @media ( max-width: 480px ) {
521 + .os-welcome {
522 + padding: 16px;
500 523 }
501 - .desktop-mode-welcome__body,
502 - .desktop-mode-welcome__actions {
503 - padding-left: 24px;
504 - padding-right: 24px;
524 + .os-welcome .os-welcome__mark {
525 + top: 24px;
526 + inset-inline-start: 24px;
505 527 }
506 - .desktop-mode-welcome__title {
507 - font-size: 22px;
528 + .os-welcome__content {
529 + padding: 24px;
508 530 }
509 - .desktop-mode-welcome__actions {
510 - flex-direction: column-reverse;
511 - align-items: stretch;
531 + .os-welcome__actions {
532 + padding: 0 24px 24px;
512 533 }
513 - .desktop-mode-welcome__btn {
514 - width: 100%;
534 + .os-welcome .os-welcome__btn {
535 + flex: 1 1 auto;
515 536 }
516 537 }
517 538 @media ( prefers-reduced-motion: reduce ) {
518 - .desktop-mode-welcome,
519 - .desktop-mode-welcome__card,
520 - .desktop-mode-welcome__hero,
521 - .desktop-mode-welcome__hint-icon {
539 + .os-welcome,
540 + .os-welcome__card {
522 541 animation: none !important;
523 542 }
543 + .os-welcome .os-welcome__btn {
544 + transition: none;
545 + }
524 546 }
525 547 </style>
526 548 <div
527 - class="desktop-mode-welcome"
549 + class="os-welcome"
528 550 role="dialog"
529 551 aria-modal="true"
530 - aria-labelledby="desktop-mode-welcome-title"
531 - aria-describedby="desktop-mode-welcome-desc"
552 + aria-labelledby="os-welcome-title"
553 + aria-describedby="os-welcome-desc"
532 554 data-slug="<?php echo esc_attr( $slug ); ?>"
533 555 >
534 - <div class="desktop-mode-welcome__card">
535 - <button
536 - type="button"
537 - class="desktop-mode-welcome__close"
538 - aria-label="<?php echo esc_attr( $close_a ); ?>"
539 - data-desktop-mode-welcome-dismiss
540 - >&times;</button>
541 - <div class="desktop-mode-welcome__hero">
542 - <span class="desktop-mode-welcome__eyebrow">
543 - <span class="desktop-mode-welcome__eyebrow-dot" aria-hidden="true"></span>
544 - <?php echo esc_html__( 'New here', 'desktop-mode' ); ?>
545 - </span>
546 - <h2 id="desktop-mode-welcome-title" class="desktop-mode-welcome__title">
547 - <?php echo esc_html( $title ); ?>
548 - </h2>
549 - <p class="desktop-mode-welcome__subtitle">
550 - <?php echo esc_html( $subtitle ); ?>
551 - </p>
556 + <div class="os-welcome__card">
557 + <div class="os-welcome__art" aria-hidden="true">
558 + <img class="os-welcome__mark" src="<?php echo esc_url( $mark_url ); ?>" alt="" width="40" height="40" />
559 + <div class="os-welcome__pair">
560 + <div class="os-welcome__win os-welcome__win--posts">
561 + <div class="os-welcome__bar">
562 + <span class="os-welcome__bar-title"><i></i><?php echo esc_html__( 'Posts', 'desktop-mode' ); ?></span>
563 + <span class="os-welcome__bar-ctl"><i></i><i></i><i></i></span>
564 + </div>
565 + <div class="os-welcome__rows">
566 + <div class="os-welcome__row os-welcome__row--head"><i></i><b style="width:40%"></b><em></em></div>
567 + <div class="os-welcome__row"><i></i><b style="width:78%"></b><em></em></div>
568 + <div class="os-welcome__row os-welcome__row--selected"><i></i><b style="width:62%"></b><em></em></div>
569 + <div class="os-welcome__row"><i></i><b style="width:84%"></b><em></em></div>
570 + <div class="os-welcome__row"><i></i><b style="width:55%"></b><em></em></div>
571 + <div class="os-welcome__row"><i></i><b style="width:70%"></b><em></em></div>
572 + </div>
573 + </div>
574 + <div class="os-welcome__win os-welcome__win--media">
575 + <div class="os-welcome__bar">
576 + <span class="os-welcome__bar-title"><i></i><?php echo esc_html__( 'Media', 'desktop-mode' ); ?></span>
577 + <span class="os-welcome__bar-ctl"><i></i><i></i><i></i></span>
578 + </div>
579 + <div class="os-welcome__thumbs">
580 + <i></i><i></i><i></i><i></i>
581 + <i></i><i></i><i></i><i></i>
582 + </div>
583 + </div>
584 + </div>
552 585 </div>
553 - <div class="desktop-mode-welcome__body">
554 - <p id="desktop-mode-welcome-desc" class="desktop-mode-welcome__lede">
555 - <?php echo esc_html( $body ); ?>
556 - </p>
557 - <ul class="desktop-mode-welcome__features">
558 - <?php foreach ( $features as $feature ) : ?>
559 - <li class="desktop-mode-welcome__feature">
560 - <span class="desktop-mode-welcome__feature-icon" aria-hidden="true">
561 - <?php echo esc_html( $feature['icon'] ); ?>
562 - </span>
563 - <strong class="desktop-mode-welcome__feature-title">
564 - <?php echo esc_html( $feature['title'] ); ?>
565 - </strong>
566 - <p class="desktop-mode-welcome__feature-desc">
567 - <?php echo esc_html( $feature['desc'] ); ?>
568 - </p>
569 - </li>
570 - <?php endforeach; ?>
571 - </ul>
572 - <p class="desktop-mode-welcome__hint">
573 - <span class="desktop-mode-welcome__hint-icon" aria-hidden="true"></span>
574 - <?php
575 - printf(
576 - /* translators: %s: the label of the admin bar button. */
577 - esc_html__( 'Prefer to switch yourself? Click %s in the top-right of your admin bar.', 'desktop-mode' ),
578 - '<code>' . esc_html__( 'Switch to Desktop Mode', 'desktop-mode' ) . '</code>'
579 - );
580 - ?>
581 - </p>
586 + <div class="os-welcome__main">
587 + <div class="os-welcome__content">
588 + <div class="os-welcome__head">
589 + <h2 id="os-welcome-title" class="os-welcome__title">
590 + <?php echo esc_html( $title ); ?>
591 + </h2>
592 + <p id="os-welcome-desc" class="os-welcome__lede">
593 + <?php echo esc_html( $body ); ?>
594 + </p>
595 + </div>
596 + <ul class="os-welcome__features">
597 + <?php foreach ( $features as $feature ) : ?>
598 + <li class="os-welcome__feature">
599 + <span class="os-welcome__icon" aria-hidden="true">
600 + <?php echo openstation_welcome_dialog_icon( $feature['icon'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- sanitised with wp_kses() in the helper. ?>
601 + </span>
602 + <div>
603 + <strong class="os-welcome__feature-title">
604 + <?php
605 + if ( ! empty( $feature['kbd'] ) ) {
606 + echo wp_kses(
607 + sprintf(
608 + esc_html( $feature['title'] ),
609 + '<kbd class="os-welcome__kbd" data-os-welcome-shortcut>⌘K</kbd>'
610 + ),
611 + array(
612 + 'kbd' => array(
613 + 'class' => true,
614 + 'data-*' => true,
615 + ),
616 + )
617 + );
618 + } else {
619 + echo esc_html( $feature['title'] );
620 + }
621 + ?>
622 + </strong>
623 + <p class="os-welcome__feature-desc">
624 + <?php echo esc_html( $feature['desc'] ); ?>
625 + </p>
626 + </div>
627 + </li>
628 + <?php endforeach; ?>
629 + </ul>
630 + </div>
631 + <div class="os-welcome__actions">
632 + <button
633 + type="button"
634 + class="os-welcome__btn os-welcome__btn--secondary"
635 + data-os-welcome-cta
636 + >
637 + <?php echo esc_html( $later ); ?>
638 + </button>
639 + <button
640 + type="button"
641 + class="os-welcome__btn os-welcome__btn--primary"
642 + data-os-welcome-enable
643 + data-label-idle="<?php echo esc_attr( $enable ); ?>"
644 + data-label-busy="<?php echo esc_attr( $enabling ); ?>"
645 + >
646 + <?php echo esc_html( $enable ); ?>
647 + </button>
648 + </div>
582 649 </div>
583 - <div class="desktop-mode-welcome__actions">
584 - <button
585 - type="button"
586 - class="desktop-mode-welcome__btn desktop-mode-welcome__btn--secondary"
587 - data-desktop-mode-welcome-cta
588 - >
589 - <?php echo esc_html( $cta ); ?>
590 - </button>
591 - <button
592 - type="button"
593 - class="desktop-mode-welcome__btn desktop-mode-welcome__btn--primary"
594 - data-desktop-mode-welcome-enable
595 - data-label-idle="<?php echo esc_attr( $enable ); ?>"
596 - data-label-busy="<?php echo esc_attr( $enabling ); ?>"
597 - >
598 - <?php echo esc_html( $enable ); ?>
599 - </button>
600 - </div>
601 650 </div>
602 651 </div>
603 -<script id="desktop-mode-welcome-script">
652 +<script id="os-welcome-script">
604 653 ( function () {
605 - var root = document.querySelector( '.desktop-mode-welcome' );
654 + var root = document.querySelector( '.os-welcome' );
606 655 if ( ! root ) {
607 656 return;
608 657 }
609 658 var cfg = {
@@ -613,13 +662,19 @@
613 662 ajaxUrl: <?php echo wp_json_encode( $ajax_url ); ?>,
614 663 ajaxNonce: <?php echo wp_json_encode( $ajax_nonce ); ?>,
615 664 };
616 665
617 - document.body.classList.add( 'desktop-mode-welcome-open' );
666 + document.body.classList.add( 'os-welcome-open' );
618 667
668 + // The shell answers Cmd+K and Ctrl+K alike; show the one this keyboard has.
669 + var shortcut = root.querySelector( '[data-os-welcome-shortcut]' );
670 + if ( shortcut && ! /Mac|iPhone|iPad|iPod/.test( navigator.platform || navigator.userAgent ) ) {
671 + shortcut.textContent = 'Ctrl K';
672 + }
673 +
619 674 // Focus the primary CTA so keyboard users land somewhere meaningful.
620 - var primary = root.querySelector( '[data-desktop-mode-welcome-enable]' )
621 - || root.querySelector( '[data-desktop-mode-welcome-cta]' );
675 + var primary = root.querySelector( '[data-os-welcome-enable]' )
676 + || root.querySelector( '[data-os-welcome-cta]' );
622 677 if ( primary ) {
623 678 try { primary.focus( { preventScroll: true } ); } catch ( e ) {}
624 679 }
625 680
@@ -627,12 +682,33 @@
627 682 if ( ! root || ! root.parentNode ) {
628 683 return;
629 684 }
630 685 root.parentNode.removeChild( root );
631 - document.body.classList.remove( 'desktop-mode-welcome-open' );
686 + document.body.classList.remove( 'os-welcome-open' );
632 687 document.removeEventListener( 'keydown', onKey );
633 688 }
634 689
690 + // Rebuilds an absolute URL onto the origin the admin page was actually
691 + // loaded from. `rest_url()` / `admin_url()` are pinned to `site_url()`,
692 + // but the admin may be viewed through a different origin — a reverse
693 + // proxy, a Flexible-SSL edge, a mapped multisite domain, or simply an
694 + // HTTPS dev proxy in front of an HTTP site. POSTing the *absolute*
695 + // site_url URL from such a page is cross-origin (and mixed-content when
696 + // the page is HTTPS and site_url is HTTP); the browser blocks it, the
697 + // dismissal never reaches the server, and the dialog re-renders on every
698 + // page load. Reissuing the request to `window.location.origin` keeps it
699 + // same-origin — where the logged-in cookie (domain-scoped, not
700 + // port-scoped) and the `wp_rest` nonce (session-bound, origin-agnostic)
701 + // are both valid.
702 + function sameOrigin( url ) {
703 + try {
704 + var parsed = new URL( url, window.location.href );
705 + return window.location.origin + parsed.pathname + parsed.search;
706 + } catch ( e ) {
707 + return url;
708 + }
709 + }
710 +
635 711 function persist() {
636 712 // Fire-and-forget. The seen-intros endpoint always returns the
637 713 // post-mutation list, but we don't need it here; if the request
638 714 // fails (offline, REST disabled) the dialog will simply show
@@ -638,26 +714,43 @@
638 714 // fails (offline, REST disabled) the dialog will simply show
639 715 // again next page load — exactly the behavior a user would
640 716 // expect from a "save my dismissal" call that didn't reach the
641 717 // server.
642 - //
643 - // `keepalive: true` keeps the POST alive across the navigation
644 - // that "Enable it now" triggers — otherwise the redirect into the
645 - // shell can abort the in-flight request and the dismissal is lost.
646 - // (The `desktop_mode_is_enabled()` gate already stops the dialog
647 - // re-rendering in the shell, but this keeps the seen-state correct
648 - // for the classic admin too, e.g. if the user switches back.)
718 + var url = sameOrigin( cfg.url );
719 + var payload = JSON.stringify( { slug: cfg.slug } );
720 +
721 + // Prefer `navigator.sendBeacon`: it is queued by the browser and
722 + // survives the navigation that "Switch to OpenStation" triggers
723 + // without the keepalive caveats, and it is inherently
724 + // same-origin-credentialed. The `wp_rest` nonce rides along as
725 + // `_wpnonce` (REST cookie auth reads it from `$_REQUEST`), and the
726 + // Blob's `application/json` type lets the REST server parse the
727 + // `slug` body param.
649 728 try {
729 + if ( navigator.sendBeacon ) {
730 + var beaconUrl = url +
731 + ( url.indexOf( '?' ) === -1 ? '?' : '&' ) +
732 + '_wpnonce=' + encodeURIComponent( cfg.nonce );
733 + var blob = new Blob( [ payload ], { type: 'application/json' } );
734 + if ( navigator.sendBeacon( beaconUrl, blob ) ) {
735 + return;
736 + }
737 + }
738 + } catch ( e ) {}
739 +
740 + // Fallback: `keepalive: true` keeps the POST alive across the
741 + // "Switch to OpenStation" redirect on browsers without sendBeacon.
742 + try {
650 743 var headers = { 'Content-Type': 'application/json' };
651 744 if ( cfg.nonce ) {
652 745 headers[ 'X-WP-Nonce' ] = cfg.nonce;
653 746 }
654 - fetch( cfg.url, {
747 + fetch( url, {
655 748 method: 'POST',
656 749 credentials: 'same-origin',
657 750 keepalive: true,
658 751 headers: headers,
659 - body: JSON.stringify( { slug: cfg.slug } ),
752 + body: payload,
660 753 } ).catch( function () {} );
661 754 } catch ( e ) {}
662 755 }
663 756
@@ -683,13 +776,13 @@
683 776 // need to see the welcome again.
684 777 persist();
685 778
686 779 var form = new FormData();
687 - form.append( 'action', 'save-desktop-mode' );
780 + form.append( 'action', 'save-openstation' );
688 781 form.append( 'nonce', cfg.ajaxNonce );
689 782 form.append( 'enabled', '1' );
690 783
691 - fetch( cfg.ajaxUrl, {
784 + fetch( sameOrigin( cfg.ajaxUrl ), {
692 785 method: 'POST',
693 786 credentials: 'same-origin',
694 787 body: form,
695 788 } ).then( function ( r ) {
@@ -703,9 +796,9 @@
703 796 return;
704 797 }
705 798 // Fallback — reload so the shell takes over (the AJAX endpoint
706 799 // already wrote the user meta, so this request will boot
707 - // straight into Desktop Mode via the portal flow).
800 + // straight into OpenStation via the portal flow).
708 801 window.location.reload();
709 802 } ).catch( function () {
710 803 enabling = false;
711 804 btn.disabled = false;
@@ -717,16 +810,15 @@
717 810 var target = event.target;
718 811 if ( ! ( target instanceof Element ) ) {
719 812 return;
720 813 }
721 - var enableBtn = target.closest( '[data-desktop-mode-welcome-enable]' );
814 + var enableBtn = target.closest( '[data-os-welcome-enable]' );
722 815 if ( enableBtn ) {
723 816 event.preventDefault();
724 817 enableNow( enableBtn );
725 818 return;
726 819 }
727 - if ( target.closest( '[data-desktop-mode-welcome-dismiss]' ) ||
728 - target.closest( '[data-desktop-mode-welcome-cta]' ) ) {
820 + if ( target.closest( '[data-os-welcome-cta]' ) ) {
729 821 event.preventDefault();
730 822 dismiss();
731 823 return;
732 824 }
@@ -747,5 +839,5 @@
747 839 } )();
748 840 </script>
749 841 <?php
750 842 }
751 -add_action( 'admin_footer', 'desktop_mode_render_welcome_dialog' );
843 +add_action( 'admin_footer', 'openstation_render_welcome_dialog' );