PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.8
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.8, at includes/welcome-dialog.php

782 lines 24.0 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 */
21
22 defined( 'ABSPATH' ) || exit;
23
24 /** Slug stored in `desktop_mode_seen_intros` for this dialog. */
25 const DESKTOP_MODE_WELCOME_INTRO_SLUG = 'activation-welcome';
26
27 /**
28 * Decides whether the welcome dialog should render on the current request.
29 *
30 * Six gates:
31 *
32 * 1. We're inside `/wp-admin` (`is_admin()`).
33 * 2. The user is logged in and can `read` (sanity gate — the dialog has
34 * no destructive surface, but anonymous output makes no sense).
35 * 3. The request is NOT chromeless — chromeless pages are iframes
36 * rendering inside the desktop shell; the parent shell already shows
37 * its own UX.
38 * 4. Desktop Mode is NOT already enabled for the user. This is a
39 * "switch to Desktop Mode" promo, so it has nothing to say once the
40 * user is in the shell. The desktop shell's *parent* page is admin
41 * 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.
45 * 5. The user has not already dismissed this intro.
46 * 6. The `desktop_mode_show_welcome_dialog` filter returns truthy, so
47 * sites can suppress the dialog entirely (e.g. managed-host onboarding
48 * flows that ship their own).
49 *
50 * @return bool
51 */
52 function desktop_mode_should_show_welcome_dialog() {
53 if ( ! is_admin() || ! is_user_logged_in() ) {
54 return false;
55 }
56 if ( ! current_user_can( 'read' ) ) {
57 return false;
58 }
59 if ( function_exists( 'desktop_mode_is_chromeless_request' ) && desktop_mode_is_chromeless_request() ) {
60 return false;
61 }
62 if ( function_exists( 'desktop_mode_is_enabled' ) && desktop_mode_is_enabled() ) {
63 return false;
64 }
65 $user_id = get_current_user_id();
66 if ( desktop_mode_has_seen_intro( $user_id, DESKTOP_MODE_WELCOME_INTRO_SLUG ) ) {
67 return false;
68 }
69
70 /**
71 * Filters whether the first-run welcome dialog should render for
72 * the current user on the current request. All earlier gates
73 * (admin context, capability, chromeless, seen-state) have
74 * already passed by the time this filter fires.
75 *
76 * @param bool $show Whether to render the dialog. Default true.
77 * @param int $user_id Current user ID.
78 */
79 return (bool) apply_filters( 'desktop_mode_show_welcome_dialog', true, $user_id );
80 }
81
82 /**
83 * Prints the welcome dialog markup, styles and dismiss script into
84 * `admin_footer`.
85 *
86 * Self-contained on purpose: everything is scoped under the
87 * `.desktop-mode-welcome` namespace so it cannot collide with the host
88 * admin theme. The dismiss button POSTs to the seen-intros REST route,
89 * which is exactly the same endpoint the in-shell intros use.
90 */
91 function desktop_mode_render_welcome_dialog() {
92 if ( ! desktop_mode_should_show_welcome_dialog() ) {
93 return;
94 }
95
96 $rest_url = esc_url_raw( rest_url( 'desktop-mode/v1/intros/seen' ) );
97 $rest_nonce = wp_create_nonce( 'wp_rest' );
98 $ajax_url = esc_url_raw( admin_url( 'admin-ajax.php' ) );
99 $ajax_nonce = wp_create_nonce( 'save-desktop-mode' );
100 $slug = DESKTOP_MODE_WELCOME_INTRO_SLUG;
101
102 // All user-facing strings are passed through translation; the dialog
103 // is keyboard-dismissible (Escape) and moves initial focus to the
104 // primary CTA.
105 $title = __( 'Welcome to Desktop Mode', '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 = __( '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' );
108 $cta = __( 'Got it', 'desktop-mode' );
109 $enable = __( 'Enable it now', 'desktop-mode' );
110 $enabling = __( 'Enabling…', 'desktop-mode' );
111 $close_a = __( 'Close', 'desktop-mode' );
112
113 $features = array(
114 array(
115 'icon' => '🪟',
116 'title' => __( 'Multi-window workspace', 'desktop-mode' ),
117 'desc' => __( 'Open Posts, Media and Plugins in their own draggable, resizable windows. Tile, cascade or stack them however you work best.', 'desktop-mode' ),
118 ),
119 array(
120 'icon' => '',
121 'title' => __( 'Native, table-driven apps', 'desktop-mode' ),
122 'desc' => __( 'Posts, Pages, Users and Plugins are reimplemented as fast native windows with sticky headers, bulk actions, multi-select and inline previews.', 'desktop-mode' ),
123 ),
124 array(
125 'icon' => '🎨',
126 'title' => __( 'Wallpapers, dock & themes', 'desktop-mode' ),
127 'desc' => __( 'Make it yours. Choose a wallpaper, pin your favorite screens to the dock, and switch accent colors — all from OS Settings.', 'desktop-mode' ),
128 ),
129 array(
130 'icon' => '🤖',
131 'title' => __( 'AI Copilot, built in', 'desktop-mode' ),
132 'desc' => __( 'Press ⌘K anywhere to ask the AI Assistant — it can run commands, navigate windows and answer questions about your site.', 'desktop-mode' ),
133 ),
134 );
135 ?>
136 <style id="desktop-mode-welcome-style">
137 .desktop-mode-welcome,
138 .desktop-mode-welcome * {
139 box-sizing: border-box;
140 }
141 .desktop-mode-welcome {
142 position: fixed;
143 inset: 0;
144 z-index: 100000;
145 display: flex;
146 align-items: center;
147 justify-content: center;
148 padding: 24px;
149 background: radial-gradient(
150 ellipse at 30% 20%,
151 rgba( 56, 189, 248, 0.32 ),
152 transparent 60%
153 ),
154 radial-gradient(
155 ellipse at 70% 80%,
156 rgba( 16, 185, 129, 0.26 ),
157 transparent 55%
158 ),
159 rgba( 8, 14, 26, 0.62 );
160 backdrop-filter: blur( 14px ) saturate( 140% );
161 -webkit-backdrop-filter: blur( 14px ) saturate( 140% );
162 animation: desktop-mode-welcome-fade 360ms cubic-bezier( 0.22, 1, 0.36, 1 );
163 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
164 "Helvetica Neue", Arial, sans-serif;
165 -webkit-font-smoothing: antialiased;
166 -moz-osx-font-smoothing: grayscale;
167 }
168 @keyframes desktop-mode-welcome-fade {
169 from { opacity: 0; }
170 to { opacity: 1; }
171 }
172 @keyframes desktop-mode-welcome-pop {
173 0% { opacity: 0; transform: translateY( 14px ) scale( 0.96 ); }
174 100% { opacity: 1; transform: translateY( 0 ) scale( 1 ); }
175 }
176 @keyframes desktop-mode-welcome-arrow {
177 0%, 100% { transform: translateX( 0 ); opacity: 0.85; }
178 50% { transform: translateX( 6px ); opacity: 1; }
179 }
180 @keyframes desktop-mode-welcome-shimmer {
181 0% { background-position: 0% 50%; }
182 100% { background-position: 200% 50%; }
183 }
184 .desktop-mode-welcome__card {
185 position: relative;
186 width: 100%;
187 max-width: 760px;
188 border-radius: 22px;
189 overflow: hidden;
190 background: linear-gradient(
191 145deg,
192 rgba( 255, 255, 255, 0.98 ) 0%,
193 rgba( 244, 250, 253, 0.98 ) 100%
194 );
195 box-shadow:
196 0 1px 0 rgba( 255, 255, 255, 0.85 ) inset,
197 0 30px 60px -20px rgba( 8, 47, 73, 0.55 ),
198 0 18px 36px -18px rgba( 14, 165, 233, 0.45 );
199 animation: desktop-mode-welcome-pop 460ms cubic-bezier( 0.22, 1, 0.36, 1 ) both;
200 animation-delay: 60ms;
201 }
202 .desktop-mode-welcome__card::before {
203 content: "";
204 position: absolute;
205 inset: -1px;
206 border-radius: inherit;
207 padding: 1px;
208 background: linear-gradient(
209 135deg,
210 rgba( 14, 165, 233, 0.65 ),
211 rgba( 6, 182, 212, 0.55 ),
212 rgba( 16, 185, 129, 0.55 )
213 );
214 -webkit-mask: linear-gradient( #000 0 0 ) content-box,
215 linear-gradient( #000 0 0 );
216 -webkit-mask-composite: xor;
217 mask-composite: exclude;
218 pointer-events: none;
219 }
220 .desktop-mode-welcome__hero {
221 position: relative;
222 padding: 36px 36px 24px;
223 background: linear-gradient(
224 135deg,
225 #0f172a 0%,
226 #0e7490 45%,
227 #06b6d4 100%
228 );
229 background-size: 200% 200%;
230 animation: desktop-mode-welcome-shimmer 14s ease infinite alternate;
231 color: #fff;
232 overflow: hidden;
233 }
234 .desktop-mode-welcome__hero::after {
235 content: "";
236 position: absolute;
237 inset: 0;
238 background:
239 radial-gradient( circle at 85% 15%, rgba( 56, 189, 248, 0.35 ), transparent 45% ),
240 radial-gradient( circle at 15% 90%, rgba( 16, 185, 129, 0.22 ), transparent 50% ),
241 linear-gradient( rgba( 255, 255, 255, 0.05 ) 1px, transparent 1px ) 0 0 / 28px 28px,
242 linear-gradient( 90deg, rgba( 255, 255, 255, 0.05 ) 1px, transparent 1px ) 0 0 / 28px 28px;
243 pointer-events: none;
244 mask-image: radial-gradient( ellipse at 50% 40%, #000 35%, transparent 80% );
245 -webkit-mask-image: radial-gradient( ellipse at 50% 40%, #000 35%, transparent 80% );
246 }
247 .desktop-mode-welcome__eyebrow {
248 display: inline-flex;
249 align-items: center;
250 gap: 8px;
251 padding: 5px 12px;
252 font-size: 11px;
253 font-weight: 600;
254 letter-spacing: 0.12em;
255 text-transform: uppercase;
256 color: #fff;
257 background: rgba( 255, 255, 255, 0.18 );
258 border: 1px solid rgba( 255, 255, 255, 0.28 );
259 border-radius: 999px;
260 backdrop-filter: blur( 6px );
261 -webkit-backdrop-filter: blur( 6px );
262 }
263 .desktop-mode-welcome__eyebrow-dot {
264 width: 6px;
265 height: 6px;
266 border-radius: 50%;
267 background: #22d3ee;
268 box-shadow: 0 0 0 4px rgba( 34, 211, 238, 0.28 );
269 }
270 .desktop-mode-welcome__title {
271 margin: 14px 0 6px;
272 font-size: 26px;
273 line-height: 1.2;
274 font-weight: 700;
275 letter-spacing: -0.01em;
276 color: #fff;
277 }
278 .desktop-mode-welcome__subtitle {
279 margin: 0;
280 font-size: 14px;
281 line-height: 1.5;
282 color: rgba( 255, 255, 255, 0.85 );
283 }
284 .desktop-mode-welcome__body {
285 padding: 24px 36px 24px;
286 font-size: 14.5px;
287 line-height: 1.6;
288 color: #1f2937;
289 }
290 .desktop-mode-welcome__lede {
291 margin: 0;
292 font-size: 14.5px;
293 color: #374151;
294 }
295 .desktop-mode-welcome__features {
296 display: grid;
297 grid-template-columns: repeat( 2, minmax( 0, 1fr ) );
298 gap: 14px;
299 margin: 20px 0 0;
300 padding: 0;
301 list-style: none;
302 }
303 .desktop-mode-welcome__feature {
304 position: relative;
305 padding: 14px 14px 14px 52px;
306 background: linear-gradient(
307 145deg,
308 rgba( 255, 255, 255, 0.85 ),
309 rgba( 240, 249, 255, 0.85 )
310 );
311 border: 1px solid rgba( 14, 165, 233, 0.18 );
312 border-radius: 12px;
313 box-shadow: 0 1px 0 rgba( 255, 255, 255, 0.9 ) inset,
314 0 2px 6px -2px rgba( 8, 47, 73, 0.08 );
315 }
316 .desktop-mode-welcome__feature-icon {
317 position: absolute;
318 top: 12px;
319 left: 12px;
320 width: 30px;
321 height: 30px;
322 display: inline-flex;
323 align-items: center;
324 justify-content: center;
325 border-radius: 9px;
326 background: linear-gradient( 135deg, rgba( 14, 165, 233, 0.16 ), rgba( 16, 185, 129, 0.16 ) );
327 border: 1px solid rgba( 14, 165, 233, 0.22 );
328 font-size: 16px;
329 line-height: 1;
330 }
331 .desktop-mode-welcome__feature-title {
332 display: block;
333 margin: 0 0 2px;
334 font-size: 13.5px;
335 font-weight: 700;
336 color: #0f172a;
337 }
338 .desktop-mode-welcome__feature-desc {
339 margin: 0;
340 font-size: 12.5px;
341 line-height: 1.5;
342 color: #475569;
343 }
344 .desktop-mode-welcome__hint {
345 display: flex;
346 align-items: center;
347 gap: 12px;
348 margin: 18px 0 0;
349 padding: 14px 16px;
350 background: linear-gradient(
351 135deg,
352 rgba( 14, 165, 233, 0.08 ),
353 rgba( 16, 185, 129, 0.08 )
354 );
355 border: 1px solid rgba( 14, 165, 233, 0.22 );
356 border-radius: 12px;
357 font-size: 13.5px;
358 color: #0c4a6e;
359 }
360 .desktop-mode-welcome__hint-icon {
361 flex-shrink: 0;
362 width: 24px;
363 height: 24px;
364 border-radius: 50%;
365 display: inline-flex;
366 align-items: center;
367 justify-content: center;
368 background: linear-gradient( 135deg, #0ea5e9, #06b6d4 );
369 color: #fff;
370 font-size: 14px;
371 line-height: 1;
372 animation: desktop-mode-welcome-arrow 1.8s ease-in-out infinite;
373 }
374 .desktop-mode-welcome__hint-icon::before {
375 content: "";
376 font-weight: 700;
377 }
378 .desktop-mode-welcome__hint code {
379 display: inline-block;
380 padding: 2px 6px;
381 margin: 0 2px;
382 background: rgba( 255, 255, 255, 0.85 );
383 border: 1px solid rgba( 14, 165, 233, 0.28 );
384 border-radius: 5px;
385 font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
386 font-size: 12px;
387 color: #0369a1;
388 }
389 .desktop-mode-welcome__actions {
390 display: flex;
391 align-items: center;
392 justify-content: flex-end;
393 gap: 10px;
394 padding: 0 36px 28px;
395 }
396 .desktop-mode-welcome__btn {
397 display: inline-flex;
398 align-items: center;
399 justify-content: center;
400 gap: 6px;
401 min-height: 38px;
402 padding: 8px 18px;
403 font-size: 14px;
404 font-weight: 600;
405 font-family: inherit;
406 line-height: 1;
407 border-radius: 10px;
408 border: 1px solid transparent;
409 cursor: pointer;
410 transition: transform 120ms ease, box-shadow 120ms ease,
411 background 120ms ease, color 120ms ease;
412 }
413 .desktop-mode-welcome__btn:focus-visible {
414 outline: 2px solid #0ea5e9;
415 outline-offset: 2px;
416 }
417 .desktop-mode-welcome__btn--ghost {
418 background: transparent;
419 color: #475569;
420 border-color: transparent;
421 }
422 .desktop-mode-welcome__btn--ghost:hover {
423 background: rgba( 15, 23, 42, 0.06 );
424 color: #0f172a;
425 }
426 .desktop-mode-welcome__btn--secondary {
427 color: #0369a1;
428 background: rgba( 14, 165, 233, 0.10 );
429 border-color: rgba( 14, 165, 233, 0.28 );
430 }
431 .desktop-mode-welcome__btn--secondary:hover {
432 background: rgba( 14, 165, 233, 0.16 );
433 color: #075985;
434 }
435 .desktop-mode-welcome__btn--primary {
436 color: #fff;
437 background: linear-gradient( 135deg, #0369a1, #0ea5e9 55%, #06b6d4 );
438 background-size: 180% 180%;
439 box-shadow: 0 10px 24px -10px rgba( 14, 165, 233, 0.75 ),
440 0 4px 10px -4px rgba( 6, 182, 212, 0.55 );
441 }
442 .desktop-mode-welcome__btn--primary:hover {
443 transform: translateY( -1px );
444 background-position: 100% 50%;
445 box-shadow: 0 14px 30px -12px rgba( 14, 165, 233, 0.85 ),
446 0 6px 14px -4px rgba( 6, 182, 212, 0.65 );
447 }
448 .desktop-mode-welcome__btn--primary:active {
449 transform: translateY( 0 );
450 }
451 .desktop-mode-welcome__btn[disabled] {
452 opacity: 0.65;
453 cursor: progress;
454 }
455 .desktop-mode-welcome__close {
456 position: absolute;
457 top: 14px;
458 right: 14px;
459 width: 32px;
460 height: 32px;
461 display: inline-flex;
462 align-items: center;
463 justify-content: center;
464 padding: 0;
465 background: rgba( 255, 255, 255, 0.18 );
466 color: #fff;
467 border: 1px solid rgba( 255, 255, 255, 0.25 );
468 border-radius: 50%;
469 cursor: pointer;
470 font-size: 18px;
471 line-height: 1;
472 transition: background 120ms ease, transform 120ms ease;
473 }
474 .desktop-mode-welcome__close:hover {
475 background: rgba( 255, 255, 255, 0.32 );
476 transform: rotate( 90deg );
477 }
478 .desktop-mode-welcome__close:focus-visible {
479 outline: 2px solid #fff;
480 outline-offset: 2px;
481 }
482 body.desktop-mode-welcome-open {
483 overflow: hidden;
484 }
485 @media ( max-width: 760px ) {
486 .desktop-mode-welcome__features {
487 grid-template-columns: 1fr;
488 }
489 }
490 @media ( max-width: 600px ) {
491 .desktop-mode-welcome__hero {
492 padding: 28px 24px 20px;
493 }
494 .desktop-mode-welcome__body,
495 .desktop-mode-welcome__actions {
496 padding-left: 24px;
497 padding-right: 24px;
498 }
499 .desktop-mode-welcome__title {
500 font-size: 22px;
501 }
502 .desktop-mode-welcome__actions {
503 flex-direction: column-reverse;
504 align-items: stretch;
505 }
506 .desktop-mode-welcome__btn {
507 width: 100%;
508 }
509 }
510 @media ( prefers-reduced-motion: reduce ) {
511 .desktop-mode-welcome,
512 .desktop-mode-welcome__card,
513 .desktop-mode-welcome__hero,
514 .desktop-mode-welcome__hint-icon {
515 animation: none !important;
516 }
517 }
518 </style>
519 <div
520 class="desktop-mode-welcome"
521 role="dialog"
522 aria-modal="true"
523 aria-labelledby="desktop-mode-welcome-title"
524 aria-describedby="desktop-mode-welcome-desc"
525 data-slug="<?php echo esc_attr( $slug ); ?>"
526 >
527 <div class="desktop-mode-welcome__card">
528 <button
529 type="button"
530 class="desktop-mode-welcome__close"
531 aria-label="<?php echo esc_attr( $close_a ); ?>"
532 data-desktop-mode-welcome-dismiss
533 >&times;</button>
534 <div class="desktop-mode-welcome__hero">
535 <span class="desktop-mode-welcome__eyebrow">
536 <span class="desktop-mode-welcome__eyebrow-dot" aria-hidden="true"></span>
537 <?php echo esc_html__( 'New here', 'desktop-mode' ); ?>
538 </span>
539 <h2 id="desktop-mode-welcome-title" class="desktop-mode-welcome__title">
540 <?php echo esc_html( $title ); ?>
541 </h2>
542 <p class="desktop-mode-welcome__subtitle">
543 <?php echo esc_html( $subtitle ); ?>
544 </p>
545 </div>
546 <div class="desktop-mode-welcome__body">
547 <p id="desktop-mode-welcome-desc" class="desktop-mode-welcome__lede">
548 <?php echo esc_html( $body ); ?>
549 </p>
550 <ul class="desktop-mode-welcome__features">
551 <?php foreach ( $features as $feature ) : ?>
552 <li class="desktop-mode-welcome__feature">
553 <span class="desktop-mode-welcome__feature-icon" aria-hidden="true">
554 <?php echo esc_html( $feature['icon'] ); ?>
555 </span>
556 <strong class="desktop-mode-welcome__feature-title">
557 <?php echo esc_html( $feature['title'] ); ?>
558 </strong>
559 <p class="desktop-mode-welcome__feature-desc">
560 <?php echo esc_html( $feature['desc'] ); ?>
561 </p>
562 </li>
563 <?php endforeach; ?>
564 </ul>
565 <p class="desktop-mode-welcome__hint">
566 <span class="desktop-mode-welcome__hint-icon" aria-hidden="true"></span>
567 <?php
568 printf(
569 /* translators: %s: the label of the admin bar button. */
570 esc_html__( 'Prefer to switch yourself? Click %s in the top-right of your admin bar.', 'desktop-mode' ),
571 '<code>' . esc_html__( 'Switch to Desktop Mode', 'desktop-mode' ) . '</code>'
572 );
573 ?>
574 </p>
575 </div>
576 <div class="desktop-mode-welcome__actions">
577 <button
578 type="button"
579 class="desktop-mode-welcome__btn desktop-mode-welcome__btn--secondary"
580 data-desktop-mode-welcome-cta
581 >
582 <?php echo esc_html( $cta ); ?>
583 </button>
584 <button
585 type="button"
586 class="desktop-mode-welcome__btn desktop-mode-welcome__btn--primary"
587 data-desktop-mode-welcome-enable
588 data-label-idle="<?php echo esc_attr( $enable ); ?>"
589 data-label-busy="<?php echo esc_attr( $enabling ); ?>"
590 >
591 <?php echo esc_html( $enable ); ?>
592 </button>
593 </div>
594 </div>
595 </div>
596 <script id="desktop-mode-welcome-script">
597 ( function () {
598 var root = document.querySelector( '.desktop-mode-welcome' );
599 if ( ! root ) {
600 return;
601 }
602 var cfg = {
603 url: <?php echo wp_json_encode( $rest_url ); ?>,
604 nonce: <?php echo wp_json_encode( $rest_nonce ); ?>,
605 slug: <?php echo wp_json_encode( $slug ); ?>,
606 ajaxUrl: <?php echo wp_json_encode( $ajax_url ); ?>,
607 ajaxNonce: <?php echo wp_json_encode( $ajax_nonce ); ?>,
608 };
609
610 document.body.classList.add( 'desktop-mode-welcome-open' );
611
612 // Focus the primary CTA so keyboard users land somewhere meaningful.
613 var primary = root.querySelector( '[data-desktop-mode-welcome-enable]' )
614 || root.querySelector( '[data-desktop-mode-welcome-cta]' );
615 if ( primary ) {
616 try { primary.focus( { preventScroll: true } ); } catch ( e ) {}
617 }
618
619 function close() {
620 if ( ! root || ! root.parentNode ) {
621 return;
622 }
623 root.parentNode.removeChild( root );
624 document.body.classList.remove( 'desktop-mode-welcome-open' );
625 document.removeEventListener( 'keydown', onKey );
626 }
627
628 // Rebuilds an absolute URL onto the origin the admin page was actually
629 // loaded from. `rest_url()` / `admin_url()` are pinned to `site_url()`,
630 // but the admin may be viewed through a different origin — a reverse
631 // proxy, a Flexible-SSL edge, a mapped multisite domain, or simply an
632 // HTTPS dev proxy in front of an HTTP site. POSTing the *absolute*
633 // site_url URL from such a page is cross-origin (and mixed-content when
634 // the page is HTTPS and site_url is HTTP); the browser blocks it, the
635 // dismissal never reaches the server, and the dialog re-renders on every
636 // page load. Reissuing the request to `window.location.origin` keeps it
637 // same-origin — where the logged-in cookie (domain-scoped, not
638 // port-scoped) and the `wp_rest` nonce (session-bound, origin-agnostic)
639 // are both valid.
640 function sameOrigin( url ) {
641 try {
642 var parsed = new URL( url, window.location.href );
643 return window.location.origin + parsed.pathname + parsed.search;
644 } catch ( e ) {
645 return url;
646 }
647 }
648
649 function persist() {
650 // Fire-and-forget. The seen-intros endpoint always returns the
651 // post-mutation list, but we don't need it here; if the request
652 // fails (offline, REST disabled) the dialog will simply show
653 // again next page load — exactly the behavior a user would
654 // expect from a "save my dismissal" call that didn't reach the
655 // server.
656 var url = sameOrigin( cfg.url );
657 var payload = JSON.stringify( { slug: cfg.slug } );
658
659 // Prefer `navigator.sendBeacon`: it is queued by the browser and
660 // survives the navigation that "Enable it now" triggers without the
661 // keepalive caveats, and it is inherently same-origin-credentialed.
662 // The `wp_rest` nonce rides along as `_wpnonce` (REST cookie auth
663 // reads it from `$_REQUEST`), and the Blob's `application/json` type
664 // lets the REST server parse the `slug` body param.
665 try {
666 if ( navigator.sendBeacon ) {
667 var beaconUrl = url +
668 ( url.indexOf( '?' ) === -1 ? '?' : '&' ) +
669 '_wpnonce=' + encodeURIComponent( cfg.nonce );
670 var blob = new Blob( [ payload ], { type: 'application/json' } );
671 if ( navigator.sendBeacon( beaconUrl, blob ) ) {
672 return;
673 }
674 }
675 } catch ( e ) {}
676
677 // Fallback: `keepalive: true` keeps the POST alive across the
678 // "Enable it now" redirect on browsers without sendBeacon.
679 try {
680 var headers = { 'Content-Type': 'application/json' };
681 if ( cfg.nonce ) {
682 headers[ 'X-WP-Nonce' ] = cfg.nonce;
683 }
684 fetch( url, {
685 method: 'POST',
686 credentials: 'same-origin',
687 keepalive: true,
688 headers: headers,
689 body: payload,
690 } ).catch( function () {} );
691 } catch ( e ) {}
692 }
693
694 function dismiss() {
695 persist();
696 close();
697 }
698
699 var enabling = false;
700
701 function enableNow( btn ) {
702 if ( enabling ) {
703 return;
704 }
705 enabling = true;
706 var idle = btn.getAttribute( 'data-label-idle' ) || btn.textContent;
707 var busy = btn.getAttribute( 'data-label-busy' ) || idle;
708 btn.disabled = true;
709 btn.textContent = busy;
710
711 // Persist the dismissal in parallel — even if the AJAX save fails
712 // the user explicitly asked to turn the mode on, so they don't
713 // need to see the welcome again.
714 persist();
715
716 var form = new FormData();
717 form.append( 'action', 'save-desktop-mode' );
718 form.append( 'nonce', cfg.ajaxNonce );
719 form.append( 'enabled', '1' );
720
721 fetch( sameOrigin( cfg.ajaxUrl ), {
722 method: 'POST',
723 credentials: 'same-origin',
724 body: form,
725 } ).then( function ( r ) {
726 return r.json().catch( function () { return null; } );
727 } ).then( function ( data ) {
728 var redirect = data && data.success && data.data && data.data.redirect
729 ? data.data.redirect
730 : null;
731 if ( redirect ) {
732 window.location.href = redirect;
733 return;
734 }
735 // Fallback — reload so the shell takes over (the AJAX endpoint
736 // already wrote the user meta, so this request will boot
737 // straight into Desktop Mode via the portal flow).
738 window.location.reload();
739 } ).catch( function () {
740 enabling = false;
741 btn.disabled = false;
742 btn.textContent = idle;
743 } );
744 }
745
746 root.addEventListener( 'click', function ( event ) {
747 var target = event.target;
748 if ( ! ( target instanceof Element ) ) {
749 return;
750 }
751 var enableBtn = target.closest( '[data-desktop-mode-welcome-enable]' );
752 if ( enableBtn ) {
753 event.preventDefault();
754 enableNow( enableBtn );
755 return;
756 }
757 if ( target.closest( '[data-desktop-mode-welcome-dismiss]' ) ||
758 target.closest( '[data-desktop-mode-welcome-cta]' ) ) {
759 event.preventDefault();
760 dismiss();
761 return;
762 }
763 // Backdrop click — outside the card.
764 if ( target === root ) {
765 event.preventDefault();
766 dismiss();
767 }
768 } );
769
770 function onKey( event ) {
771 if ( event.key === 'Escape' || event.key === 'Esc' ) {
772 event.preventDefault();
773 dismiss();
774 }
775 }
776 document.addEventListener( 'keydown', onKey );
777 } )();
778 </script>
779 <?php
780 }
781 add_action( 'admin_footer', 'desktop_mode_render_welcome_dialog' );
782