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 +447 -357 1.1.61.1.10 View file →
@@ -1,15 +1,16 @@
1 1 <?php
2 2 /**
3 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 OpenStation" 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 OpenStation Preferences →
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 16 * `<os-*>` shell components here because they only ship inside the
@@ -38,11 +39,12 @@
38 39 * 4. OpenStation is NOT already enabled for the user. This is a
39 40 * "switch to OpenStation" promo, so it has nothing to say once the
40 41 * user is in the shell. The desktop shell's *parent* page is admin
41 42 * context and is not chromeless, so without this gate the dialog
42 - * re-renders there the moment the user clicks "Enable it now" — read
43 - * as a duplicate dialog, because the fire-and-forget seen-intro POST
44 - * 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.
45 47 * 5. The user has not already dismissed this intro.
46 48 * 6. The `openstation_show_welcome_dialog` filter returns truthy, so
47 49 * sites can suppress the dialog entirely (e.g. managed-host onboarding
48 50 * flows that ship their own).
@@ -79,8 +81,46 @@
79 81 return (bool) apply_filters( 'openstation_show_welcome_dialog', true, $user_id );
80 82 }
81 83
82 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 +/**
83 123 * Prints the welcome dialog markup, styles and dismiss script into
84 124 * `admin_footer`.
85 125 *
86 126 * Self-contained on purpose: everything is scoped under the
@@ -86,8 +126,14 @@
86 126 * Self-contained on purpose: everything is scoped under the
87 127 * `.os-welcome` namespace so it cannot collide with the host
88 128 * admin theme. The dismiss button POSTs to the seen-intros REST route,
89 129 * which is exactly the same endpoint the in-shell intros use.
130 + *
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.
90 136 */
91 137 function openstation_render_welcome_dialog() {
92 138 if ( ! openstation_should_show_welcome_dialog() ) {
93 139 return;
@@ -97,71 +143,94 @@
97 143 $rest_nonce = wp_create_nonce( 'wp_rest' );
98 144 $ajax_url = esc_url_raw( admin_url( 'admin-ajax.php' ) );
99 145 $ajax_nonce = wp_create_nonce( 'save-openstation' );
100 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';
101 150
102 151 // All user-facing strings are passed through translation; the dialog
103 152 // is keyboard-dismissible (Escape) and moves initial focus to the
104 153 // primary CTA.
105 154 $title = __( 'Welcome to OpenStation', 'desktop-mode' );
106 - $subtitle = __( 'A floating, window-based workspace for the WordPress admin — more like a real OS, less like a webpage.', 'desktop-mode' );
107 - $body = __( 'OpenStation 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' );
108 - $cta = __( 'Got it', 'desktop-mode' );
109 - $enable = __( 'Enable it now', 'desktop-mode' );
110 - $enabling = __( 'Enabling…', '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' );
111 159
112 160 $features = array(
113 161 array(
114 - 'icon' => '🪟',
115 - 'title' => __( 'Multi-window workspace', 'desktop-mode' ),
116 - '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' ),
117 165 ),
118 166 array(
119 - 'icon' => '⚡',
120 - 'title' => __( 'Native, table-driven apps', 'desktop-mode' ),
121 - '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' ),
122 170 ),
123 171 array(
124 - 'icon' => '🎨',
125 - 'title' => __( 'Wallpapers, dock & themes', 'desktop-mode' ),
126 - 'desc' => __( 'Make it yours. Choose a wallpaper, pin your favorite screens to the dock, and switch accent colors — all from OpenStation Preferences.', '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' ),
127 175 ),
128 176 array(
129 - 'icon' => '🤖',
130 - 'title' => __( 'AI Copilot, built in', 'desktop-mode' ),
131 - '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,
132 182 ),
133 183 );
134 184 ?>
135 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 + }
136 200 .os-welcome,
137 201 .os-welcome * {
138 202 box-sizing: border-box;
139 203 }
140 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 +
141 219 position: fixed;
142 220 inset: 0;
143 221 z-index: 100000;
144 222 display: flex;
145 - align-items: center;
146 - justify-content: center;
147 223 padding: 24px;
148 - background: radial-gradient(
149 - ellipse at 30% 20%,
150 - rgba( 236, 155, 255, 0.32 ),
151 - transparent 60%
152 - ),
153 - radial-gradient(
154 - ellipse at 70% 80%,
155 - rgba( 147, 240, 198, 0.26 ),
156 - transparent 55%
157 - ),
158 - rgba( 12, 11, 15, 0.62 );
159 - backdrop-filter: blur( 14px ) saturate( 140% );
160 - -webkit-backdrop-filter: blur( 14px ) saturate( 140% );
161 - animation: os-welcome-fade 360ms cubic-bezier( 0.22, 1, 0.36, 1 );
162 - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
163 - "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 );
164 233 -webkit-font-smoothing: antialiased;
165 234 -moz-osx-font-smoothing: grayscale;
166 235 }
167 236 @keyframes os-welcome-fade {
@@ -168,332 +237,313 @@
168 237 from { opacity: 0; }
169 238 to { opacity: 1; }
170 239 }
171 240 @keyframes os-welcome-pop {
172 - 0% { opacity: 0; transform: translateY( 14px ) scale( 0.96 ); }
173 - 100% { opacity: 1; transform: translateY( 0 ) scale( 1 ); }
241 + from { opacity: 0; transform: translateY( 8px ); }
242 + to { opacity: 1; transform: translateY( 0 ); }
174 243 }
175 - @keyframes os-welcome-arrow {
176 - 0%, 100% { transform: translateX( 0 ); opacity: 0.85; }
177 - 50% { transform: translateX( 6px ); opacity: 1; }
178 - }
179 - @keyframes os-welcome-shimmer {
180 - 0% { background-position: 0% 50%; }
181 - 100% { background-position: 100% 50%; }
182 - }
183 244 .os-welcome__card {
184 - position: relative;
245 + /* margin:auto centres the card and still lets a short viewport scroll to its top. */
246 + margin: auto;
185 247 width: 100%;
186 - max-width: 760px;
187 - border-radius: 22px;
248 + max-width: 820px;
249 + min-height: 544px;
250 + display: grid;
251 + grid-template-columns: 5fr 6fr;
188 252 overflow: hidden;
189 - background: linear-gradient(
190 - 145deg,
191 - rgba( 255, 255, 255, 0.98 ) 0%,
192 - rgba( 255, 251, 255, 0.98 ) 100%
193 - );
253 + background: var( --_starlight );
254 + border-radius: 13px;
194 255 box-shadow:
195 - 0 1px 0 rgba( 255, 255, 255, 0.85 ) inset,
196 - 0 30px 60px -20px rgba( 26, 23, 33, 0.55 ),
197 - 0 18px 36px -18px rgba( 242, 82, 252, 0.45 );
198 - animation: os-welcome-pop 460ms cubic-bezier( 0.22, 1, 0.36, 1 ) both;
199 - 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;
200 259 }
201 - .os-welcome__card::before {
202 - content: "";
203 - position: absolute;
204 - inset: -1px;
205 - border-radius: inherit;
206 - padding: 1px;
207 - background: linear-gradient(
208 - 135deg,
209 - rgba( 242, 82, 252, 0.65 ),
210 - rgba( 236, 155, 255, 0.55 ),
211 - rgba( 147, 240, 198, 0.55 )
212 - );
213 - -webkit-mask: linear-gradient( #000 0 0 ) content-box,
214 - linear-gradient( #000 0 0 );
215 - -webkit-mask-composite: xor;
216 - mask-composite: exclude;
217 - pointer-events: none;
218 - }
219 - .os-welcome__hero {
260 +
261 + /* ---- Dark panel: two windows on the station ------------------- */
262 +
263 + .os-welcome__art {
220 264 position: relative;
221 - padding: 36px 36px 24px;
222 - background: linear-gradient(
223 - 135deg,
224 - #0c0b0f 0%,
225 - #2a2533 45%,
226 - #ec9bff 100%
227 - );
228 - background-size: 200% 200%;
229 - animation: os-welcome-shimmer 14s ease infinite alternate;
230 - color: #fff;
231 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 );
232 280 }
233 - .os-welcome__hero::after {
281 + .os-welcome__art::after {
234 282 content: "";
235 283 position: absolute;
236 284 inset: 0;
285 + pointer-events: none;
237 286 background:
238 - radial-gradient( circle at 85% 15%, rgba( 236, 155, 255, 0.35 ), transparent 45% ),
239 - radial-gradient( circle at 15% 90%, rgba( 147, 240, 198, 0.22 ), transparent 50% ),
240 - linear-gradient( rgba( 255, 255, 255, 0.05 ) 1px, transparent 1px ) 0 0 / 28px 28px,
241 - linear-gradient( 90deg, rgba( 255, 255, 255, 0.05 ) 1px, transparent 1px ) 0 0 / 28px 28px;
242 - pointer-events: none;
243 - mask-image: radial-gradient( ellipse at 50% 40%, #000 35%, transparent 80% );
244 - -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% );
245 289 }
246 - .os-welcome__eyebrow {
247 - 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;
248 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;
249 342 gap: 8px;
250 - padding: 5px 12px;
251 - font-size: 11px;
252 - font-weight: 600;
253 - letter-spacing: 0.12em;
254 - text-transform: uppercase;
255 - color: #fff;
256 - background: rgba( 255, 255, 255, 0.18 );
257 - border: 1px solid rgba( 255, 255, 255, 0.28 );
258 - border-radius: 999px;
259 - backdrop-filter: blur( 6px );
260 - -webkit-backdrop-filter: blur( 6px );
343 + height: 24px;
344 + border-bottom: 1px solid rgba( 255, 251, 255, 0.07 );
261 345 }
262 - .os-welcome__eyebrow-dot {
263 - width: 6px;
264 - height: 6px;
265 - border-radius: 50%;
266 - background: #ec9bff;
267 - box-shadow: 0 0 0 4px rgba( 236, 155, 255, 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 );
268 359 }
269 - .os-welcome__title {
270 - margin: 14px 0 6px;
271 - font-size: 26px;
272 - line-height: 1.2;
273 - 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;
274 391 letter-spacing: -0.01em;
275 - color: #fff;
392 + color: var( --_obsidian );
393 + text-wrap: balance;
276 394 }
277 - .os-welcome__subtitle {
395 + .os-welcome .os-welcome__lede {
278 396 margin: 0;
279 - font-size: 14px;
397 + font-size: 16px;
280 398 line-height: 1.5;
281 - color: rgba( 255, 255, 255, 0.85 );
399 + color: var( --_silver );
282 400 }
283 - .os-welcome__body {
284 - padding: 24px 36px 24px;
285 - font-size: 14.5px;
286 - line-height: 1.6;
287 - color: #1a1721;
288 - }
289 - .os-welcome__lede {
401 + .os-welcome .os-welcome__features {
402 + display: grid;
403 + gap: 16px;
290 404 margin: 0;
291 - font-size: 14.5px;
292 - color: #33303a;
293 - }
294 - .os-welcome__features {
295 - display: grid;
296 - grid-template-columns: repeat( 2, minmax( 0, 1fr ) );
297 - gap: 14px;
298 - margin: 20px 0 0;
299 405 padding: 0;
300 406 list-style: none;
301 407 }
302 - .os-welcome__feature {
303 - position: relative;
304 - padding: 14px 14px 14px 52px;
305 - background: linear-gradient(
306 - 145deg,
307 - rgba( 255, 255, 255, 0.85 ),
308 - rgba( 255, 251, 255, 0.85 )
309 - );
310 - border: 1px solid rgba( 242, 82, 252, 0.18 );
311 - border-radius: 12px;
312 - box-shadow: 0 1px 0 rgba( 255, 255, 255, 0.9 ) inset,
313 - 0 2px 6px -2px rgba( 26, 23, 33, 0.08 );
408 + .os-welcome .os-welcome__feature {
409 + display: flex;
410 + gap: 16px;
411 + margin: 0;
314 412 }
315 - .os-welcome__feature-icon {
316 - position: absolute;
317 - top: 12px;
318 - left: 12px;
319 - width: 30px;
320 - height: 30px;
321 - display: inline-flex;
322 - align-items: center;
323 - justify-content: center;
324 - border-radius: 9px;
325 - background: linear-gradient( 135deg, rgba( 242, 82, 252, 0.16 ), rgba( 147, 240, 198, 0.16 ) );
326 - border: 1px solid rgba( 242, 82, 252, 0.22 );
327 - font-size: 16px;
328 - line-height: 1;
413 + .os-welcome__icon {
414 + flex: none;
415 + width: 20px;
416 + height: 20px;
417 + color: var( --_pulse );
329 418 }
419 + .os-welcome__icon svg {
420 + display: block;
421 + width: 20px;
422 + height: 20px;
423 + }
330 424 .os-welcome__feature-title {
331 425 display: block;
332 - margin: 0 0 2px;
333 - font-size: 13.5px;
334 - font-weight: 700;
335 - color: #0c0b0f;
426 + font-size: 14px;
427 + font-weight: 600;
428 + line-height: 1.4;
429 + color: var( --_obsidian );
336 430 }
337 - .os-welcome__feature-desc {
431 + .os-welcome .os-welcome__feature-desc {
338 432 margin: 0;
339 - font-size: 12.5px;
433 + font-size: 14px;
340 434 line-height: 1.5;
341 - color: #b3afb5;
435 + color: var( --_pewter );
342 436 }
343 - .os-welcome__hint {
344 - display: flex;
345 - align-items: center;
346 - gap: 12px;
347 - margin: 18px 0 0;
348 - padding: 14px 16px;
349 - background: linear-gradient(
350 - 135deg,
351 - rgba( 242, 82, 252, 0.08 ),
352 - rgba( 147, 240, 198, 0.08 )
353 - );
354 - border: 1px solid rgba( 242, 82, 252, 0.22 );
355 - border-radius: 12px;
356 - font-size: 13.5px;
357 - color: #0c0b0f;
358 - }
359 - .os-welcome__hint-icon {
360 - flex-shrink: 0;
361 - width: 24px;
362 - height: 24px;
363 - border-radius: 50%;
364 - display: inline-flex;
365 - align-items: center;
366 - justify-content: center;
367 - background: linear-gradient( 135deg, #f252fc, #ec9bff );
368 - color: #fff;
369 - font-size: 14px;
370 - line-height: 1;
371 - animation: os-welcome-arrow 1.8s ease-in-out infinite;
372 - }
373 - .os-welcome__hint-icon::before {
374 - content: "→";
375 - font-weight: 700;
376 - }
377 - .os-welcome__hint code {
437 + .os-welcome .os-welcome__kbd {
378 438 display: inline-block;
379 - padding: 2px 6px;
380 - margin: 0 2px;
381 - background: rgba( 255, 255, 255, 0.85 );
382 - border: 1px solid rgba( 242, 82, 252, 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 );
383 449 border-radius: 5px;
384 - font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
385 - font-size: 12px;
386 - color: #2a2533;
450 + white-space: nowrap;
387 451 }
452 +
453 + /* Anchored to the bottom of the card, however long the copy runs. */
388 454 .os-welcome__actions {
455 + margin-top: auto;
389 456 display: flex;
390 - align-items: center;
457 + flex-wrap: wrap;
391 458 justify-content: flex-end;
392 - gap: 10px;
393 - padding: 0 36px 28px;
459 + gap: 8px;
460 + padding: 0 32px 32px;
394 461 }
395 - .os-welcome__btn {
462 + .os-welcome .os-welcome__btn {
396 463 display: inline-flex;
397 464 align-items: center;
398 465 justify-content: center;
399 - gap: 6px;
400 - min-height: 38px;
401 - padding: 8px 18px;
466 + min-height: 40px;
467 + margin: 0;
468 + padding: 0 16px;
469 + font-family: inherit;
402 470 font-size: 14px;
403 - font-weight: 600;
404 - font-family: inherit;
471 + font-weight: 500;
405 472 line-height: 1;
406 - border-radius: 10px;
407 - border: 1px solid transparent;
473 + border-radius: 8px;
474 + border: 1px solid var( --_obsidian );
475 + box-shadow: none;
408 476 cursor: pointer;
409 - transition: transform 120ms ease, box-shadow 120ms ease,
410 - background 120ms ease, color 120ms ease;
477 + transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
411 478 }
412 - .os-welcome__btn:focus-visible {
413 - outline: 2px solid #f252fc;
479 + .os-welcome .os-welcome__btn:focus-visible {
480 + outline: 2px solid var( --_obsidian );
414 481 outline-offset: 2px;
415 482 }
416 - .os-welcome__btn--ghost {
417 - background: transparent;
418 - color: #b3afb5;
419 - border-color: transparent;
483 + .os-welcome .os-welcome__btn--primary {
484 + color: var( --_starlight );
485 + background: var( --_obsidian );
420 486 }
421 - .os-welcome__btn--ghost:hover {
422 - background: rgba( 12, 11, 15, 0.06 );
423 - color: #0c0b0f;
487 + .os-welcome .os-welcome__btn--primary:hover {
488 + background: var( --_astro );
489 + border-color: var( --_astro );
424 490 }
425 - .os-welcome__btn--secondary {
426 - color: #2a2533;
427 - background: rgba( 242, 82, 252, 0.10 );
428 - border-color: rgba( 242, 82, 252, 0.28 );
491 + .os-welcome .os-welcome__btn--secondary {
492 + color: var( --_obsidian );
493 + background: transparent;
429 494 }
430 - .os-welcome__btn--secondary:hover {
431 - background: rgba( 242, 82, 252, 0.16 );
432 - color: #1a1721;
495 + .os-welcome .os-welcome__btn--secondary:hover {
496 + background: var( --_haze );
433 497 }
434 - .os-welcome__btn--primary {
435 - color: #fffbff;
436 - /*
437 - * Ends on Pulse, not Nebula. `background-position` slides this
438 - * on hover, so every stop carries the label at some point, and
439 - * Nebula is light enough that Starlight text on it falls to
440 - * ~1.6:1. White-on-Pulse is what <os-button variant="primary">
441 - * already uses.
442 - */
443 - background: linear-gradient( 135deg, #2a2533, #a12bb0 45%, #f252fc );
444 - background-size: 180% 180%;
445 - box-shadow: 0 10px 24px -10px rgba( 242, 82, 252, 0.75 ),
446 - 0 4px 10px -4px rgba( 236, 155, 255, 0.55 );
447 - }
448 - .os-welcome__btn--primary:hover {
449 - transform: translateY( -1px );
450 - background-position: 100% 50%;
451 - box-shadow: 0 14px 30px -12px rgba( 242, 82, 252, 0.85 ),
452 - 0 6px 14px -4px rgba( 236, 155, 255, 0.65 );
453 - }
454 - .os-welcome__btn--primary:active {
455 - transform: translateY( 0 );
456 - }
457 - .os-welcome__btn[disabled] {
458 - opacity: 0.65;
498 + .os-welcome .os-welcome__btn[disabled] {
499 + opacity: 0.64;
459 500 cursor: progress;
460 501 }
461 502 body.os-welcome-open {
462 503 overflow: hidden;
463 504 }
505 +
506 + /* ---- Narrow screens ------------------------------------------- */
507 +
464 508 @media ( max-width: 760px ) {
465 - .os-welcome__features {
509 + .os-welcome__card {
466 510 grid-template-columns: 1fr;
511 + min-height: 0;
467 512 }
513 + .os-welcome__art {
514 + height: 216px;
515 + }
516 + .os-welcome__pair {
517 + transform: translate( -50%, -44% ) scale( 0.64 );
518 + }
468 519 }
469 - @media ( max-width: 600px ) {
470 - .os-welcome__hero {
471 - padding: 28px 24px 20px;
520 + @media ( max-width: 480px ) {
521 + .os-welcome {
522 + padding: 16px;
472 523 }
473 - .os-welcome__body,
474 - .os-welcome__actions {
475 - padding-left: 24px;
476 - padding-right: 24px;
524 + .os-welcome .os-welcome__mark {
525 + top: 24px;
526 + inset-inline-start: 24px;
477 527 }
478 - .os-welcome__title {
479 - font-size: 22px;
528 + .os-welcome__content {
529 + padding: 24px;
480 530 }
481 531 .os-welcome__actions {
482 - flex-direction: column-reverse;
483 - align-items: stretch;
532 + padding: 0 24px 24px;
484 533 }
485 - .os-welcome__btn {
486 - width: 100%;
534 + .os-welcome .os-welcome__btn {
535 + flex: 1 1 auto;
487 536 }
488 537 }
489 538 @media ( prefers-reduced-motion: reduce ) {
490 539 .os-welcome,
491 - .os-welcome__card,
492 - .os-welcome__hero,
493 - .os-welcome__hint-icon {
540 + .os-welcome__card {
494 541 animation: none !important;
495 542 }
543 + .os-welcome .os-welcome__btn {
544 + transition: none;
545 + }
496 546 }
497 547 </style>
498 548 <div
499 549 class="os-welcome"
@@ -503,68 +553,101 @@
503 553 aria-describedby="os-welcome-desc"
504 554 data-slug="<?php echo esc_attr( $slug ); ?>"
505 555 >
506 556 <div class="os-welcome__card">
507 - <div class="os-welcome__hero">
508 - <span class="os-welcome__eyebrow">
509 - <span class="os-welcome__eyebrow-dot" aria-hidden="true"></span>
510 - <?php echo esc_html__( 'New here', 'desktop-mode' ); ?>
511 - </span>
512 - <h2 id="os-welcome-title" class="os-welcome__title">
513 - <?php echo esc_html( $title ); ?>
514 - </h2>
515 - <p class="os-welcome__subtitle">
516 - <?php echo esc_html( $subtitle ); ?>
517 - </p>
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>
518 585 </div>
519 - <div class="os-welcome__body">
520 - <p id="os-welcome-desc" class="os-welcome__lede">
521 - <?php echo esc_html( $body ); ?>
522 - </p>
523 - <ul class="os-welcome__features">
524 - <?php foreach ( $features as $feature ) : ?>
525 - <li class="os-welcome__feature">
526 - <span class="os-welcome__feature-icon" aria-hidden="true">
527 - <?php echo esc_html( $feature['icon'] ); ?>
528 - </span>
529 - <strong class="os-welcome__feature-title">
530 - <?php echo esc_html( $feature['title'] ); ?>
531 - </strong>
532 - <p class="os-welcome__feature-desc">
533 - <?php echo esc_html( $feature['desc'] ); ?>
534 - </p>
535 - </li>
536 - <?php endforeach; ?>
537 - </ul>
538 - <p class="os-welcome__hint">
539 - <span class="os-welcome__hint-icon" aria-hidden="true"></span>
540 - <?php
541 - printf(
542 - /* translators: %s: the label of the admin bar button. */
543 - esc_html__( 'Prefer to switch yourself? Click %s in the top-right of your admin bar.', 'desktop-mode' ),
544 - '<code>' . esc_html__( 'Switch to OpenStation', 'desktop-mode' ) . '</code>'
545 - );
546 - ?>
547 - </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>
548 649 </div>
549 - <div class="os-welcome__actions">
550 - <button
551 - type="button"
552 - class="os-welcome__btn os-welcome__btn--secondary"
553 - data-os-welcome-cta
554 - >
555 - <?php echo esc_html( $cta ); ?>
556 - </button>
557 - <button
558 - type="button"
559 - class="os-welcome__btn os-welcome__btn--primary"
560 - data-os-welcome-enable
561 - data-label-idle="<?php echo esc_attr( $enable ); ?>"
562 - data-label-busy="<?php echo esc_attr( $enabling ); ?>"
563 - >
564 - <?php echo esc_html( $enable ); ?>
565 - </button>
566 - </div>
567 650 </div>
568 651 </div>
569 652 <script id="os-welcome-script">
570 653 ( function () {
@@ -581,8 +664,14 @@
581 664 };
582 665
583 666 document.body.classList.add( 'os-welcome-open' );
584 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 +
585 674 // Focus the primary CTA so keyboard users land somewhere meaningful.
586 675 var primary = root.querySelector( '[data-os-welcome-enable]' )
587 676 || root.querySelector( '[data-os-welcome-cta]' );
588 677 if ( primary ) {
@@ -629,13 +718,14 @@
629 718 var url = sameOrigin( cfg.url );
630 719 var payload = JSON.stringify( { slug: cfg.slug } );
631 720
632 721 // Prefer `navigator.sendBeacon`: it is queued by the browser and
633 - // survives the navigation that "Enable it now" triggers without the
634 - // keepalive caveats, and it is inherently same-origin-credentialed.
635 - // The `wp_rest` nonce rides along as `_wpnonce` (REST cookie auth
636 - // reads it from `$_REQUEST`), and the Blob's `application/json` type
637 - // lets the REST server parse the `slug` body param.
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.
638 728 try {
639 729 if ( navigator.sendBeacon ) {
640 730 var beaconUrl = url +
641 731 ( url.indexOf( '?' ) === -1 ? '?' : '&' ) +
@@ -647,9 +737,9 @@
647 737 }
648 738 } catch ( e ) {}
649 739
650 740 // Fallback: `keepalive: true` keeps the POST alive across the
651 - // "Enable it now" redirect on browsers without sendBeacon.
741 + // "Switch to OpenStation" redirect on browsers without sendBeacon.
652 742 try {
653 743 var headers = { 'Content-Type': 'application/json' };
654 744 if ( cfg.nonce ) {
655 745 headers[ 'X-WP-Nonce' ] = cfg.nonce;