PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.3
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.3
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
desktop-mode / includes / welcome-dialog.php

welcome-dialog.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.3, at includes/welcome-dialog.php

789 lines 24.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — First-run welcome dialog.
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
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.
12 *
13 * The dialog is intentionally self-contained — all HTML, CSS and JS are
14 * inlined into `admin_footer`. We deliberately do NOT use any of the
15 * `<wpd-*>` shell components here because they only ship inside the
16 * desktop bundle, which is precisely *not* loaded on the classic admin
17 * screens where this dialog is allowed to appear.
18 *
19 * @package Desktop_Mode
20 * @since 0.8.2
21 */
22
23 defined( 'ABSPATH' ) || exit;
24
25 /** Slug stored in `desktop_mode_seen_intros` for this dialog. */
26 const DESKTOP_MODE_WELCOME_INTRO_SLUG = 'activation-welcome';
27
28 /**
29 * Decides whether the welcome dialog should render on the current request.
30 *
31 * Six gates:
32 *
33 * 1. We're inside `/wp-admin` (`is_admin()`).
34 * 2. The user is logged in and can `read` (sanity gate — the dialog has
35 * no destructive surface, but anonymous output makes no sense).
36 * 3. The request is NOT chromeless — chromeless pages are iframes
37 * rendering inside the desktop shell; the parent shell already shows
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
41 * user is in the shell. The desktop shell's *parent* page is admin
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.
46 * 5. The user has not already dismissed this intro.
47 * 6. The `desktop_mode_show_welcome_dialog` filter returns truthy, so
48 * sites can suppress the dialog entirely (e.g. managed-host onboarding
49 * flows that ship their own).
50 *
51 * @since 0.8.2
52 *
53 * @return bool
54 */
55 function desktop_mode_should_show_welcome_dialog() {
56 if ( ! is_admin() || ! is_user_logged_in() ) {
57 return false;
58 }
59 if ( ! current_user_can( 'read' ) ) {
60 return false;
61 }
62 if ( function_exists( 'desktop_mode_is_chromeless_request' ) && desktop_mode_is_chromeless_request() ) {
63 return false;
64 }
65 if ( function_exists( 'desktop_mode_is_enabled' ) && desktop_mode_is_enabled() ) {
66 return false;
67 }
68 $user_id = get_current_user_id();
69 if ( desktop_mode_has_seen_intro( $user_id, DESKTOP_MODE_WELCOME_INTRO_SLUG ) ) {
70 return false;
71 }
72
73 /**
74 * Filters whether the first-run welcome dialog should render for
75 * the current user on the current request. All earlier gates
76 * (admin context, capability, chromeless, seen-state) have
77 * already passed by the time this filter fires.
78 *
79 * @since 0.8.2
80 *
81 * @param bool $show Whether to render the dialog. Default true.
82 * @param int $user_id Current user ID.
83 */
84 return (bool) apply_filters( 'desktop_mode_show_welcome_dialog', true, $user_id );
85 }
86
87 /**
88 * Prints the welcome dialog markup, styles and dismiss script into
89 * `admin_footer`.
90 *
91 * Self-contained on purpose: everything is scoped under the
92 * `.desktop-mode-welcome` namespace so it cannot collide with the host
93 * admin theme. The dismiss button POSTs to the seen-intros REST route,
94 * which is exactly the same endpoint the in-shell intros use.
95 *
96 * @since 0.8.2
97 */
98 function desktop_mode_render_welcome_dialog() {
99 if ( ! desktop_mode_should_show_welcome_dialog() ) {
100 return;
101 }
102
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;
108
109 // All user-facing strings are passed through translation; the dialog
110 // is keyboard-dismissible (Escape) and moves initial focus to the
111 // primary 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' );
119
120 $features = array(
121 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' ),
125 ),
126 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' ),
130 ),
131 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' ),
135 ),
136 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' ),
140 ),
141 );
142 ?>
143 <style id="desktop-mode-welcome-style">
144 .desktop-mode-welcome,
145 .desktop-mode-welcome * {
146 box-sizing: border-box;
147 }
148 .desktop-mode-welcome {
149 position: fixed;
150 inset: 0;
151 z-index: 100000;
152 display: flex;
153 align-items: center;
154 justify-content: center;
155 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;
172 -webkit-font-smoothing: antialiased;
173 -moz-osx-font-smoothing: grayscale;
174 }
175 @keyframes desktop-mode-welcome-fade {
176 from { opacity: 0; }
177 to { opacity: 1; }
178 }
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 ); }
182 }
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;
193 width: 100%;
194 max-width: 760px;
195 border-radius: 22px;
196 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 );
202 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;
208 }
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 {
228 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 overflow: hidden;
240 }
241 .desktop-mode-welcome__hero::after {
242 content: "";
243 position: absolute;
244 inset: 0;
245 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% );
253 }
254 .desktop-mode-welcome__eyebrow {
255 display: inline-flex;
256 align-items: center;
257 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 );
269 }
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 );
276 }
277 .desktop-mode-welcome__title {
278 margin: 14px 0 6px;
279 font-size: 26px;
280 line-height: 1.2;
281 font-weight: 700;
282 letter-spacing: -0.01em;
283 color: #fff;
284 }
285 .desktop-mode-welcome__subtitle {
286 margin: 0;
287 font-size: 14px;
288 line-height: 1.5;
289 color: rgba( 255, 255, 255, 0.85 );
290 }
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 {
298 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 padding: 0;
308 list-style: none;
309 }
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 );
322 }
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;
337 }
338 .desktop-mode-welcome__feature-title {
339 display: block;
340 margin: 0 0 2px;
341 font-size: 13.5px;
342 font-weight: 700;
343 color: #0f172a;
344 }
345 .desktop-mode-welcome__feature-desc {
346 margin: 0;
347 font-size: 12.5px;
348 line-height: 1.5;
349 color: #475569;
350 }
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 {
386 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 );
391 border-radius: 5px;
392 font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
393 font-size: 12px;
394 color: #0369a1;
395 }
396 .desktop-mode-welcome__actions {
397 display: flex;
398 align-items: center;
399 justify-content: flex-end;
400 gap: 10px;
401 padding: 0 36px 28px;
402 }
403 .desktop-mode-welcome__btn {
404 display: inline-flex;
405 align-items: center;
406 justify-content: center;
407 gap: 6px;
408 min-height: 38px;
409 padding: 8px 18px;
410 font-size: 14px;
411 font-weight: 600;
412 font-family: inherit;
413 line-height: 1;
414 border-radius: 10px;
415 border: 1px solid transparent;
416 cursor: pointer;
417 transition: transform 120ms ease, box-shadow 120ms ease,
418 background 120ms ease, color 120ms ease;
419 }
420 .desktop-mode-welcome__btn:focus-visible {
421 outline: 2px solid #0ea5e9;
422 outline-offset: 2px;
423 }
424 .desktop-mode-welcome__btn--ghost {
425 background: transparent;
426 color: #475569;
427 border-color: transparent;
428 }
429 .desktop-mode-welcome__btn--ghost:hover {
430 background: rgba( 15, 23, 42, 0.06 );
431 color: #0f172a;
432 }
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 );
437 }
438 .desktop-mode-welcome__btn--secondary:hover {
439 background: rgba( 14, 165, 233, 0.16 );
440 color: #075985;
441 }
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;
460 cursor: progress;
461 }
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 {
490 overflow: hidden;
491 }
492 @media ( max-width: 760px ) {
493 .desktop-mode-welcome__features {
494 grid-template-columns: 1fr;
495 }
496 }
497 @media ( max-width: 600px ) {
498 .desktop-mode-welcome__hero {
499 padding: 28px 24px 20px;
500 }
501 .desktop-mode-welcome__body,
502 .desktop-mode-welcome__actions {
503 padding-left: 24px;
504 padding-right: 24px;
505 }
506 .desktop-mode-welcome__title {
507 font-size: 22px;
508 }
509 .desktop-mode-welcome__actions {
510 flex-direction: column-reverse;
511 align-items: stretch;
512 }
513 .desktop-mode-welcome__btn {
514 width: 100%;
515 }
516 }
517 @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 {
522 animation: none !important;
523 }
524 }
525 </style>
526 <div
527 class="desktop-mode-welcome"
528 role="dialog"
529 aria-modal="true"
530 aria-labelledby="desktop-mode-welcome-title"
531 aria-describedby="desktop-mode-welcome-desc"
532 data-slug="<?php echo esc_attr( $slug ); ?>"
533 >
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>
552 </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>
582 </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 </div>
602 </div>
603 <script id="desktop-mode-welcome-script">
604 ( function () {
605 var root = document.querySelector( '.desktop-mode-welcome' );
606 if ( ! root ) {
607 return;
608 }
609 var cfg = {
610 url: <?php echo wp_json_encode( $rest_url ); ?>,
611 nonce: <?php echo wp_json_encode( $rest_nonce ); ?>,
612 slug: <?php echo wp_json_encode( $slug ); ?>,
613 ajaxUrl: <?php echo wp_json_encode( $ajax_url ); ?>,
614 ajaxNonce: <?php echo wp_json_encode( $ajax_nonce ); ?>,
615 };
616
617 document.body.classList.add( 'desktop-mode-welcome-open' );
618
619 // 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]' );
622 if ( primary ) {
623 try { primary.focus( { preventScroll: true } ); } catch ( e ) {}
624 }
625
626 function close() {
627 if ( ! root || ! root.parentNode ) {
628 return;
629 }
630 root.parentNode.removeChild( root );
631 document.body.classList.remove( 'desktop-mode-welcome-open' );
632 document.removeEventListener( 'keydown', onKey );
633 }
634
635 // Rebuilds an absolute URL onto the origin the admin page was actually
636 // loaded from. `rest_url()` / `admin_url()` are pinned to `site_url()`,
637 // but the admin may be viewed through a different origin — a reverse
638 // proxy, a Flexible-SSL edge, a mapped multisite domain, or simply an
639 // HTTPS dev proxy in front of an HTTP site. POSTing the *absolute*
640 // site_url URL from such a page is cross-origin (and mixed-content when
641 // the page is HTTPS and site_url is HTTP); the browser blocks it, the
642 // dismissal never reaches the server, and the dialog re-renders on every
643 // page load. Reissuing the request to `window.location.origin` keeps it
644 // same-origin — where the logged-in cookie (domain-scoped, not
645 // port-scoped) and the `wp_rest` nonce (session-bound, origin-agnostic)
646 // are both valid.
647 function sameOrigin( url ) {
648 try {
649 var parsed = new URL( url, window.location.href );
650 return window.location.origin + parsed.pathname + parsed.search;
651 } catch ( e ) {
652 return url;
653 }
654 }
655
656 function persist() {
657 // Fire-and-forget. The seen-intros endpoint always returns the
658 // post-mutation list, but we don't need it here; if the request
659 // fails (offline, REST disabled) the dialog will simply show
660 // again next page load — exactly the behavior a user would
661 // expect from a "save my dismissal" call that didn't reach the
662 // server.
663 var url = sameOrigin( cfg.url );
664 var payload = JSON.stringify( { slug: cfg.slug } );
665
666 // Prefer `navigator.sendBeacon`: it is queued by the browser and
667 // survives the navigation that "Enable it now" triggers without the
668 // keepalive caveats, and it is inherently same-origin-credentialed.
669 // The `wp_rest` nonce rides along as `_wpnonce` (REST cookie auth
670 // reads it from `$_REQUEST`), and the Blob's `application/json` type
671 // lets the REST server parse the `slug` body param.
672 try {
673 if ( navigator.sendBeacon ) {
674 var beaconUrl = url +
675 ( url.indexOf( '?' ) === -1 ? '?' : '&' ) +
676 '_wpnonce=' + encodeURIComponent( cfg.nonce );
677 var blob = new Blob( [ payload ], { type: 'application/json' } );
678 if ( navigator.sendBeacon( beaconUrl, blob ) ) {
679 return;
680 }
681 }
682 } catch ( e ) {}
683
684 // Fallback: `keepalive: true` keeps the POST alive across the
685 // "Enable it now" redirect on browsers without sendBeacon.
686 try {
687 var headers = { 'Content-Type': 'application/json' };
688 if ( cfg.nonce ) {
689 headers[ 'X-WP-Nonce' ] = cfg.nonce;
690 }
691 fetch( url, {
692 method: 'POST',
693 credentials: 'same-origin',
694 keepalive: true,
695 headers: headers,
696 body: payload,
697 } ).catch( function () {} );
698 } catch ( e ) {}
699 }
700
701 function dismiss() {
702 persist();
703 close();
704 }
705
706 var enabling = false;
707
708 function enableNow( btn ) {
709 if ( enabling ) {
710 return;
711 }
712 enabling = true;
713 var idle = btn.getAttribute( 'data-label-idle' ) || btn.textContent;
714 var busy = btn.getAttribute( 'data-label-busy' ) || idle;
715 btn.disabled = true;
716 btn.textContent = busy;
717
718 // Persist the dismissal in parallel — even if the AJAX save fails
719 // the user explicitly asked to turn the mode on, so they don't
720 // need to see the welcome again.
721 persist();
722
723 var form = new FormData();
724 form.append( 'action', 'save-desktop-mode' );
725 form.append( 'nonce', cfg.ajaxNonce );
726 form.append( 'enabled', '1' );
727
728 fetch( sameOrigin( cfg.ajaxUrl ), {
729 method: 'POST',
730 credentials: 'same-origin',
731 body: form,
732 } ).then( function ( r ) {
733 return r.json().catch( function () { return null; } );
734 } ).then( function ( data ) {
735 var redirect = data && data.success && data.data && data.data.redirect
736 ? data.data.redirect
737 : null;
738 if ( redirect ) {
739 window.location.href = redirect;
740 return;
741 }
742 // Fallback — reload so the shell takes over (the AJAX endpoint
743 // already wrote the user meta, so this request will boot
744 // straight into Desktop Mode via the portal flow).
745 window.location.reload();
746 } ).catch( function () {
747 enabling = false;
748 btn.disabled = false;
749 btn.textContent = idle;
750 } );
751 }
752
753 root.addEventListener( 'click', function ( event ) {
754 var target = event.target;
755 if ( ! ( target instanceof Element ) ) {
756 return;
757 }
758 var enableBtn = target.closest( '[data-desktop-mode-welcome-enable]' );
759 if ( enableBtn ) {
760 event.preventDefault();
761 enableNow( enableBtn );
762 return;
763 }
764 if ( target.closest( '[data-desktop-mode-welcome-dismiss]' ) ||
765 target.closest( '[data-desktop-mode-welcome-cta]' ) ) {
766 event.preventDefault();
767 dismiss();
768 return;
769 }
770 // Backdrop click — outside the card.
771 if ( target === root ) {
772 event.preventDefault();
773 dismiss();
774 }
775 } );
776
777 function onKey( event ) {
778 if ( event.key === 'Escape' || event.key === 'Esc' ) {
779 event.preventDefault();
780 dismiss();
781 }
782 }
783 document.addEventListener( 'keydown', onKey );
784 } )();
785 </script>
786 <?php
787 }
788 add_action( 'admin_footer', 'desktop_mode_render_welcome_dialog' );
789