PluginProbe ʕ •ᴥ•ʔ
Presto Player / 4.5.0
Presto Player v4.5.0
4.5.0 4.4.1 4.4.0 4.3.3 4.3.2 4.3.1 4.3.0 4.2.4 4.2.3 4.2.2 4.2.0 4.2.1 trunk 1.10.0 1.10.1 1.10.2 1.11.0 1.12.0 1.13.0 1.14.0 1.14.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.13 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3-beta1 2.3.0 2.3.1 2.3.2 2.3.3 3.0.0 3.0.0-beta1 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1.0 3.1.1 3.1.2 3.1.3 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4
presto-player / inc / Services / FeatureAnnounce.php
presto-player / inc / Services Last commit date
API 2 weeks ago Abilities 2 weeks ago Blocks 1 month ago License 1 month ago OAuth 2 weeks ago AdminNotice.php 2 months ago AdminNotices.php 2 months ago AjaxActions.php 2 months ago Blocks.php 2 years ago Compatibility.php 11 months ago FeatureAnnounce.php 1 day ago Learn.php 2 weeks ago Menu.php 2 weeks ago Migrations.php 2 years ago NpsSurvey.php 7 months ago Onboarding.php 2 months ago Player.php 1 month ago PluginInstaller.php 2 weeks ago PreloadService.php 2 years ago ProCompatibility.php 1 month ago ReusableVideos.php 2 months ago RewriteRulesManager.php 2 years ago Scripts.php 2 months ago Settings.php 3 months ago Shortcodes.php 2 weeks ago Streamer.php 2 months ago Translation.php 1 month ago Usage.php 1 day ago VideoPostType.php 1 month ago
FeatureAnnounce.php
1175 lines
1 <?php
2 /**
3 * Feature announcement modal shown once after a plugin update.
4 *
5 * @package PrestoPlayer\Services
6 */
7
8 namespace PrestoPlayer\Services;
9
10 /**
11 * Shows a one-time modal introducing the headline features of a release, one
12 * slide each. Currently announces the Floating Pill skin and AI Abilities.
13 *
14 * @phpstan-type PrestoSlide array{art: string, title: string, text: string, cta?: string, url?: string, feats: array<int, array{icon: string, label: string}>}
15 */
16 class FeatureAnnounce {
17
18 /**
19 * Bump this when announcing a different feature — a new id means everyone
20 * sees the modal again, even people who dismissed the previous one.
21 */
22 const ANNOUNCEMENT_ID = 'skins_abilities_v1';
23
24 /**
25 * The release that introduced the feature being announced. Sites already on
26 * this version or newer never installed without it, so they don't need telling.
27 */
28 const ANNOUNCED_IN_VERSION = '4.5.0';
29
30 /**
31 * Last plugin version this install has seen. Empty on a fresh install.
32 */
33 const SEEN_VERSION_OPTION = 'presto_player_seen_version';
34
35 /**
36 * Announcement id waiting to be shown, set when the version changes.
37 */
38 const PENDING_OPTION = 'presto_player_pending_announcement';
39
40 /**
41 * Announcement ids the current user has closed.
42 */
43 const DISMISSED_META = 'presto_player_dismissed_announcements';
44
45 /**
46 * Nonce action for the dismiss request.
47 */
48 const NONCE_ACTION = 'presto_dismiss_announcement';
49
50 /**
51 * Resolved plugin version, cached for the request.
52 *
53 * @var string|null
54 */
55 protected $version = null;
56
57 /**
58 * Register hooks.
59 *
60 * @return void
61 */
62 public function register() {
63 add_action( 'admin_init', array( $this, 'trackVersion' ) );
64 add_action( 'admin_footer', array( $this, 'render' ) );
65 add_action( 'wp_ajax_presto_dismiss_announcement', array( $this, 'dismiss' ) );
66 }
67
68 /**
69 * Queue the announcement when the running version changes.
70 *
71 * @return void
72 */
73 public function trackVersion() {
74 $seen = get_option( self::SEEN_VERSION_OPTION, '' );
75 if ( $this->version() === $seen ) {
76 return;
77 }
78
79 // No recorded version means either a brand new install or the first load
80 // after updating into the release that added this class.
81 // onboarding_completed tells the two apart — it is backfilled for
82 // pre-4.2.0 installs, so an established site always has it set.
83 // Bail before stamping: a site whose backfill missed it would otherwise be
84 // marked as caught up and could never be told about the feature.
85 if ( '' === $seen && 'yes' !== get_option( 'presto_player_onboarding_completed', 'no' ) ) {
86 return;
87 }
88
89 update_option( self::SEEN_VERSION_OPTION, $this->version() );
90
91 // Only announce to sites that were here before the feature shipped. Without
92 // this floor, a site that installs 4.5 in 2027 and updates to 4.5.1 gets a
93 // full-screen "New in Presto Player" for something that predates it.
94 if ( '' !== $seen && version_compare( $seen, self::ANNOUNCED_IN_VERSION, '>=' ) ) {
95 return;
96 }
97
98 update_option( self::PENDING_OPTION, self::ANNOUNCEMENT_ID );
99 }
100
101 /**
102 * Whether the modal should be printed for this request.
103 *
104 * @return bool
105 */
106 protected function shouldShow() {
107 // manage_options, not the menu's publish_posts: the settings REST route
108 // rejects anyone lower, so an author following the CTA just lands on a
109 // "Couldn't load settings" page.
110 if ( ! current_user_can( 'manage_options' ) ) {
111 return false;
112 }
113
114 // Only after an update queued it — see trackVersion().
115 if ( self::ANNOUNCEMENT_ID !== get_option( self::PENDING_OPTION, '' ) ) {
116 return false;
117 }
118
119 if ( ! $this->onPrestoScreen() ) {
120 return false;
121 }
122
123 return ! in_array( self::ANNOUNCEMENT_ID, $this->dismissed(), true );
124 }
125
126 /**
127 * Whether we are on one of Presto Player's own admin screens.
128 *
129 * The announcement belongs inside the plugin — it should not follow someone
130 * around the rest of WordPress.
131 *
132 * @return bool
133 */
134 protected function onPrestoScreen() {
135 if ( ! function_exists( 'get_current_screen' ) ) {
136 return false;
137 }
138
139 $screen = get_current_screen();
140 if ( ! $screen ) {
141 return false;
142 }
143
144 // Media Hub list and editor.
145 if ( 'pp_video_block' === $screen->post_type ) {
146 return true;
147 }
148
149 // Every menu page hangs off the presto-dashboard slug, so the screen id
150 // carries it — toplevel_page_presto-dashboard and friends.
151 return false !== strpos( $screen->id, 'presto' );
152 }
153
154 /**
155 * The slides, in order. One per headline feature of the release.
156 *
157 * @return array<int, PrestoSlide>
158 */
159 protected function slides() {
160 $slides = array(
161 array(
162 'art' => 'pill',
163 'title' => __( 'Controls that float over your video', 'presto-player' ),
164 'text' => __( 'Floating Pill is a new built-in skin. The control bar lifts off the video edges into a frosted, rounded bar — the way QuickTime does it.', 'presto-player' ),
165 'feats' => array(
166 array(
167 'icon' => 'pill',
168 'label' => __( 'Detached, frosted control bar', 'presto-player' ),
169 ),
170 array(
171 'icon' => 'sliders',
172 'label' => __( 'Pick it from the Skin dropdown', 'presto-player' ),
173 ),
174 array(
175 'icon' => 'frame',
176 'label' => __( 'Set per preset, so nothing else moves', 'presto-player' ),
177 ),
178 ),
179 ),
180 );
181
182 // The skin ships to every version the plugin supports; the Abilities API
183 // only exists on WP 6.9+. Announce it only where it exists, rather than
184 // suppressing the whole dialog.
185 if ( ! function_exists( 'wp_register_ability' ) ) {
186 return $slides;
187 }
188
189 $slides[] = array(
190 'art' => 'abilities',
191 'title' => __( 'Talk to your video library', 'presto-player' ),
192 'text' => __( 'Connect Claude, ChatGPT or any MCP client to this site, then ask for what you want in plain words. You decide how much it is allowed to change.', 'presto-player' ),
193 'feats' => array(
194 array(
195 'icon' => 'wand',
196 'label' => __( 'Create a video from a link', 'presto-player' ),
197 ),
198 array(
199 'icon' => 'chart',
200 'label' => __( 'Ask for your analytics', 'presto-player' ),
201 ),
202 array(
203 'icon' => 'shield',
204 'label' => __( 'Read-only until you allow more', 'presto-player' ),
205 ),
206 ),
207 'cta' => __( 'Set up AI Abilities', 'presto-player' ),
208 'url' => admin_url( 'admin.php?page=presto-dashboard&tab=Settings&section=mcp' ),
209 );
210
211 return $slides;
212 }
213
214 /**
215 * Print the modal.
216 *
217 * @return void
218 */
219 public function render() {
220 if ( ! $this->shouldShow() ) {
221 return;
222 }
223
224 $slides = $this->slides();
225 $total = count( $slides );
226
227 ?>
228 <div class="presto-announce" role="dialog" aria-modal="true" aria-label="<?php esc_attr_e( 'New in Presto Player', 'presto-player' ); ?>">
229 <div class="presto-announce__box" tabindex="-1">
230 <button type="button" class="presto-announce__x" aria-label="<?php esc_attr_e( 'Close', 'presto-player' ); ?>">
231 <svg viewBox="0 0 20 20" width="15" height="15" aria-hidden="true" focusable="false"><path d="M5 5l10 10M15 5L5 15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
232 </button>
233
234 <?php // Swapping which child is hidden is a content change, so let it speak. ?>
235 <div class="presto-announce__slides" aria-live="polite">
236 <?php foreach ( $slides as $index => $slide ) : ?>
237 <?php $this->slide( $slide, $index + 1, $total ); ?>
238 <?php endforeach; ?>
239 </div>
240
241 <?php if ( $total > 1 ) : ?>
242 <div class="presto-announce__nav">
243 <button type="button" class="presto-announce__nav-btn" data-presto-go="-1" aria-label="<?php esc_attr_e( 'Previous', 'presto-player' ); ?>">
244 <svg viewBox="0 0 20 20" width="14" height="14" aria-hidden="true" focusable="false"><path d="M12 4.5L6.5 10l5.5 5.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
245 </button>
246 <p class="presto-announce__nav-count" aria-hidden="true">
247 <span>1</span> / <?php echo esc_html( (string) $total ); ?>
248 </p>
249 <button type="button" class="presto-announce__nav-btn" data-presto-go="1" aria-label="<?php esc_attr_e( 'Next', 'presto-player' ); ?>">
250 <svg viewBox="0 0 20 20" width="14" height="14" aria-hidden="true" focusable="false"><path d="M8 4.5L13.5 10 8 15.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
251 </button>
252 </div>
253 <?php endif; ?>
254 </div>
255 </div>
256 <?php
257
258 $this->styles();
259 $this->script();
260 }
261
262 /**
263 * Print one slide. Everything but the illustration is shared markup.
264 *
265 * @param PrestoSlide $slide Slide content.
266 * @param int $index 1-based position.
267 * @param int $total Slide count.
268 * @return void
269 */
270 protected function slide( $slide, $index, $total ) {
271 $position = sprintf(
272 /* translators: 1: current slide number, 2: total number of slides. */
273 __( '%1$d of %2$d', 'presto-player' ),
274 $index,
275 $total
276 );
277
278 ?>
279 <section class="presto-announce__slide" role="group" aria-roledescription="<?php esc_attr_e( 'slide', 'presto-player' ); ?>" aria-label="<?php echo esc_attr( $position ); ?>" <?php echo 1 === $index ? '' : 'hidden'; ?>>
280 <?php
281 if ( 'pill' === $slide['art'] ) {
282 $this->artPill();
283 } else {
284 $this->art();
285 }
286 ?>
287
288 <div class="presto-announce__body">
289 <p class="presto-announce__tag">
290 <span class="presto-announce__tag-new">
291 <?php esc_html_e( 'New in Presto Player', 'presto-player' ); ?>
292 </span>
293 <span class="presto-announce__tag-ver"><?php echo esc_html( $this->shortVersion() ); ?></span>
294 </p>
295
296 <h2 class="presto-announce__title"><?php echo esc_html( $slide['title'] ); ?></h2>
297
298 <p class="presto-announce__text"><?php echo esc_html( $slide['text'] ); ?></p>
299
300 <ul class="presto-announce__feats">
301 <?php foreach ( $slide['feats'] as $feat ) : ?>
302 <li>
303 <?php $this->icon( $feat['icon'] ); ?>
304 <span><?php echo esc_html( $feat['label'] ); ?></span>
305 </li>
306 <?php endforeach; ?>
307 </ul>
308
309 <div class="presto-announce__actions">
310 <?php if ( ! empty( $slide['cta'] ) && ! empty( $slide['url'] ) ) : ?>
311 <a class="presto-announce__cta" href="<?php echo esc_url( $slide['url'] ); ?>">
312 <?php echo esc_html( $slide['cta'] ); ?>
313 <svg viewBox="0 0 20 20" width="15" height="15" aria-hidden="true" focusable="false"><path d="M4 10h11m-4.5-4.5L15 10l-4.5 4.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
314 </a>
315 <?php endif; ?>
316 <button type="button" class="presto-announce__close">
317 <?php esc_html_e( 'Not now', 'presto-player' ); ?>
318 </button>
319 </div>
320 </div>
321 </section>
322 <?php
323 }
324
325 /**
326 * Small line icon for a feature row.
327 *
328 * @param string $name Icon key.
329 * @return void
330 */
331 protected function icon( $name ) {
332 $paths = array(
333 'wand' => '<path d="M4 16l9-9m-2-3l.9 2.1L14 7l-2.1.9L11 10l-.9-2.1L8 7l2.1-.9zM16 12l.6 1.4L18 14l-1.4.6L16 16l-.6-1.4L14 14l1.4-.6z"/>',
334 'chart' => '<path d="M4 16V9m4 7V5m4 11v-5m4 5V8"/>',
335 'shield' => '<path d="M10 3.5l5 1.8v4.2c0 3.1-2 5.6-5 7-3-1.4-5-3.9-5-7V5.3z"/><path d="M7.8 10.2L9.4 12l3-3.4"/>',
336 'pill' => '<rect x="2.5" y="7" width="15" height="6" rx="3"/><path d="M6.3 10h1.1m3.2 0h4.1"/>',
337 'sliders' => '<path d="M4 7.5h12M4 12.5h12"/><circle cx="8" cy="7.5" r="1.7"/><circle cx="13" cy="12.5" r="1.7"/>',
338 'frame' => '<path d="M4 8V5.5A1.5 1.5 0 015.5 4H8m4 0h2.5A1.5 1.5 0 0116 5.5V8m0 4v2.5a1.5 1.5 0 01-1.5 1.5H12m-4 0H5.5A1.5 1.5 0 014 14.5V12"/>',
339 );
340
341 if ( empty( $paths[ $name ] ) ) {
342 return;
343 }
344
345 printf(
346 '<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">%s</svg>',
347 $paths[ $name ] // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static markup from the map above.
348 );
349 }
350
351 /**
352 * Store the dismissal against the current user.
353 *
354 * @return void
355 */
356 public function dismiss() {
357 $nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';
358 if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) {
359 wp_send_json_error();
360 }
361
362 if ( ! current_user_can( 'manage_options' ) ) {
363 wp_send_json_error();
364 }
365
366 $dismissed = $this->dismissed();
367 if ( ! in_array( self::ANNOUNCEMENT_ID, $dismissed, true ) ) {
368 $dismissed[] = self::ANNOUNCEMENT_ID;
369 update_user_meta( get_current_user_id(), self::DISMISSED_META, $dismissed );
370 }
371
372 wp_send_json_success();
373 }
374
375 /**
376 * Announcement ids this user has already closed.
377 *
378 * @return string[]
379 */
380 protected function dismissed() {
381 $dismissed = get_user_meta( get_current_user_id(), self::DISMISSED_META, true );
382 return is_array( $dismissed ) ? $dismissed : array();
383 }
384
385 /**
386 * Full plugin version. There is no version constant in the free plugin, so
387 * fall back to the plugin header the same way the OAuth endpoints do.
388 *
389 * @return string
390 */
391 protected function version() {
392 if ( null !== $this->version ) {
393 return $this->version;
394 }
395
396 if ( defined( 'PRESTO_PLAYER_VERSION' ) ) {
397 $this->version = (string) constant( 'PRESTO_PLAYER_VERSION' );
398 return $this->version;
399 }
400
401 if ( ! function_exists( 'get_plugin_data' ) ) {
402 require_once ABSPATH . 'wp-admin/includes/plugin.php';
403 }
404
405 $data = get_plugin_data( PRESTO_PLAYER_PLUGIN_FILE, false, false );
406 $this->version = empty( $data['Version'] ) ? '' : (string) $data['Version'];
407
408 return $this->version;
409 }
410
411 /**
412 * Version without the patch part, for display — 4.3.2 becomes 4.3.
413 *
414 * @return string
415 */
416 protected function shortVersion() {
417 $parts = explode( '.', $this->version() );
418 return implode( '.', array_slice( $parts, 0, 2 ) );
419 }
420
421 /**
422 * Illustration: a video frame with the new control bar floating over it, and
423 * the Skin dropdown that switches it on.
424 *
425 * Sample content is intentionally not translatable — see art().
426 *
427 * @return void
428 */
429 protected function artPill() {
430 ?>
431 <div class="presto-announce__art" aria-hidden="true">
432 <div class="presto-announce__frame">
433 <span class="presto-announce__skinpick">Skin <em>Floating Pill</em></span>
434
435 <div class="presto-announce__pill">
436 <span class="presto-announce__pill-play"></span>
437 <span class="presto-announce__pill-bar"><i></i></span>
438 <span class="presto-announce__pill-time">1:24 / 3:38</span>
439 <span class="presto-announce__pill-dot"></span>
440 <span class="presto-announce__pill-dot"></span>
441 </div>
442 </div>
443 </div>
444 <?php
445 }
446
447 /**
448 * Illustration: the Media Hub list with an assistant adding a video to it.
449 *
450 * Sample content is intentionally not translatable. It is a product mock
451 * behind aria-hidden — the same as a screenshot — so translating it would
452 * only add contextless strings to the .pot file.
453 *
454 * @return void
455 */
456 protected function art() {
457 ?>
458 <div class="presto-announce__art" aria-hidden="true">
459 <ul class="presto-announce__list">
460 <li>
461 <span>Onboarding &middot; Lesson 1</span>
462 <em>YouTube &middot; 4:21</em>
463 </li>
464 <li>
465 <span>Onboarding &middot; Lesson 2</span>
466 <em>Bunny &middot; 12:04</em>
467 </li>
468 <li class="presto-announce__list-new">
469 <span>Product Demo 2026</span>
470 <em>YouTube &middot; 3:38</em>
471 </li>
472 </ul>
473
474 <div class="presto-announce__chat">
475 <p class="presto-announce__chat-who">Claude</p>
476 <p class="presto-announce__chat-ask">Add this video as &ldquo;Product Demo 2026&rdquo; and give me the shortcode.</p>
477 <p class="presto-announce__chat-done">Done &mdash; [presto_player id=4182]</p>
478 <p class="presto-announce__chips">
479 <span>create-video-youtube</span>
480 <span>get-video-shortcode</span>
481 </p>
482 </div>
483 </div>
484 <?php
485 }
486
487 /**
488 * Modal styles. Printed inline because this only renders once per user.
489 *
490 * @return void
491 */
492 protected function styles() {
493 ?>
494 <style>
495 .presto-announce {
496 --presto-ink: #101223;
497 --presto-muted: #656D88;
498 --presto-line: rgba( 16, 18, 35, .07 );
499 --presto-blue: #3058E5;
500 --presto-violet: #9A20F8;
501 --presto-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
502
503 position: fixed;
504 inset: 0;
505 z-index: 160000;
506 display: flex;
507 align-items: center;
508 justify-content: center;
509 padding: 24px;
510 /* Scroll the overlay, never the card — a scrollbar inside the panel
511 looks like a mistake. */
512 overflow-y: auto;
513 background: rgba( 16, 18, 35, .46 );
514 animation: presto-announce-veil .22s ease both;
515 }
516
517 .presto-announce__box {
518 position: relative;
519 overflow: hidden;
520 width: 100%;
521 max-width: 520px;
522 /* Centres while allowing the overlay to scroll instead of clipping. */
523 margin: auto;
524 border-radius: 16px;
525 background: #fff;
526 /* Layered rather than one big blur — reads lifted, not stamped on. */
527 box-shadow:
528 0 1px 2px rgba( 16, 18, 35, .05 ),
529 0 12px 24px -10px rgba( 16, 18, 35, .14 ),
530 0 44px 72px -32px rgba( 16, 18, 35, .34 );
531 animation: presto-announce-rise .34s cubic-bezier( .2, .8, .2, 1 ) both;
532 }
533
534 /* Holds focus for aria-modal without drawing a ring — it's a container,
535 not something you interact with. */
536 .presto-announce__box:focus {
537 outline: none;
538 }
539
540 /* The only brand wash: a 2px edge, flush to the top. */
541 .presto-announce__box::before {
542 content: "";
543 position: absolute;
544 top: 0;
545 left: 0;
546 right: 0;
547 height: 2px;
548 background: linear-gradient( 90deg, var( --presto-violet ), #5B37F0 52%, var( --presto-blue ) );
549 }
550
551 /* Spelled out because some admin resets give section a display value,
552 and a "hidden" slide that still renders would break the whole modal. */
553 .presto-announce__slide[hidden] {
554 display: none;
555 }
556
557 .presto-announce__slide {
558 animation: presto-announce-done .24s ease both;
559 }
560
561 /* A product shot in markup, matching the landing page: the library list
562 with an assistant working against it. Deliberately dark so the graphics
563 read as a separate section from the content below. */
564 .presto-announce__art {
565 position: relative;
566 padding: 28px 26px 26px;
567 background:
568 radial-gradient( 82% 122% at 4% -14%, rgba( 154, 32, 248, .42 ), transparent 62% ),
569 radial-gradient( 70% 108% at 99% -6%, rgba( 48, 88, 229, .4 ), transparent 60% ),
570 #0D0F1E;
571 }
572
573 /* Stand-in for the video itself, so the bar below has something to float
574 over — the whole point of the skin. */
575 .presto-announce__frame {
576 position: relative;
577 aspect-ratio: 16 / 9;
578 border-radius: 10px;
579 background:
580 radial-gradient( 58% 86% at 74% 16%, rgba( 154, 32, 248, .45 ), transparent 64% ),
581 linear-gradient( 158deg, #38416F, #1B2040 60%, #12142A );
582 box-shadow: 0 12px 30px -14px rgba( 5, 6, 15, .75 );
583 }
584
585 /* Where you turn it on: the Skin dropdown in preset settings. */
586 .presto-announce__skinpick {
587 position: absolute;
588 top: 12px;
589 left: 12px;
590 display: inline-flex;
591 align-items: center;
592 gap: 7px;
593 padding: 5px 9px;
594 border-radius: 7px;
595 background: #fff;
596 box-shadow: 0 10px 24px -12px rgba( 5, 6, 15, .8 );
597 font-size: 10.5px;
598 color: var( --presto-muted );
599 }
600
601 .presto-announce__skinpick em {
602 font-style: normal;
603 font-weight: 600;
604 color: var( --presto-ink );
605 }
606
607 .presto-announce__skinpick em::after {
608 content: "\00a0\25BE";
609 color: var( --presto-muted );
610 }
611
612 /* Inset on every side and frosted — detached from the video, not welded
613 to its bottom edge. */
614 .presto-announce__pill {
615 position: absolute;
616 right: 12px;
617 bottom: 12px;
618 left: 12px;
619 display: flex;
620 align-items: center;
621 gap: 10px;
622 padding: 8px 12px;
623 border: 1px solid rgba( 255, 255, 255, .22 );
624 border-radius: 999px;
625 background: rgba( 255, 255, 255, .16 );
626 backdrop-filter: blur( 10px );
627 box-shadow: 0 10px 24px -12px rgba( 5, 6, 15, .85 );
628 animation: presto-announce-chat .45s cubic-bezier( .2, .8, .2, 1 ) .4s both;
629 }
630
631 .presto-announce__pill-play {
632 flex: none;
633 width: 0;
634 height: 0;
635 border-style: solid;
636 border-width: 5px 0 5px 8px;
637 border-color: transparent transparent transparent #fff;
638 }
639
640 .presto-announce__pill-bar {
641 flex: 1;
642 height: 3px;
643 border-radius: 2px;
644 background: rgba( 255, 255, 255, .3 );
645 }
646
647 .presto-announce__pill-bar i {
648 display: block;
649 width: 38%;
650 height: 100%;
651 border-radius: 2px;
652 background: #fff;
653 }
654
655 .presto-announce__pill-time {
656 flex: none;
657 font-family: var( --presto-mono );
658 font-size: 10px;
659 color: rgba( 255, 255, 255, .92 );
660 }
661
662 /* Volume and fullscreen, abstracted — naming them would only add noise. */
663 .presto-announce__pill-dot {
664 flex: none;
665 width: 10px;
666 height: 10px;
667 border-radius: 3px;
668 background: rgba( 255, 255, 255, .55 );
669 }
670
671 .presto-announce__list {
672 margin: 0;
673 padding: 6px;
674 border-radius: 10px;
675 background: #fff;
676 box-shadow: 0 12px 30px -14px rgba( 5, 6, 15, .75 );
677 list-style: none;
678 }
679
680 .presto-announce__list li {
681 display: flex;
682 align-items: center;
683 justify-content: space-between;
684 gap: 12px;
685 margin: 0;
686 padding: 9px 10px;
687 border-radius: 7px;
688 font-size: 12.5px;
689 color: var( --presto-ink );
690 }
691
692 .presto-announce__list em {
693 flex: none;
694 font-family: var( --presto-mono );
695 font-size: 10.5px;
696 font-style: normal;
697 color: var( --presto-muted );
698 }
699
700 /* The row the assistant just created. */
701 .presto-announce__list-new {
702 background: rgba( 48, 88, 229, .07 );
703 font-weight: 600;
704 animation: presto-announce-row .45s ease 1.5s both;
705 }
706
707 .presto-announce__chat {
708 position: relative;
709 width: 78%;
710 margin: -16px 0 0 auto;
711 padding: 13px 14px;
712 border-radius: 11px;
713 background: #fff;
714 box-shadow: 0 16px 36px -14px rgba( 5, 6, 15, .8 );
715 animation: presto-announce-chat .45s cubic-bezier( .2, .8, .2, 1 ) .5s both;
716 }
717
718 .presto-announce__chat-who {
719 display: flex;
720 align-items: center;
721 gap: 7px;
722 margin: 0 0 9px;
723 font-size: 11.5px;
724 font-weight: 600;
725 color: var( --presto-ink );
726 }
727
728 .presto-announce__chat-who::before {
729 content: "";
730 width: 16px;
731 height: 16px;
732 border-radius: 5px;
733 background: linear-gradient( 135deg, var( --presto-violet ), var( --presto-blue ) );
734 }
735
736 .presto-announce__chat-ask {
737 margin: 0 0 8px;
738 padding: 8px 10px;
739 border-radius: 7px;
740 background: rgba( 16, 18, 35, .04 );
741 font-size: 11.5px;
742 line-height: 1.5;
743 color: var( --presto-ink );
744 }
745
746 .presto-announce__chat-done {
747 margin: 0 0 9px;
748 font-family: var( --presto-mono );
749 font-size: 10.5px;
750 line-height: 1.5;
751 color: var( --presto-muted );
752 animation: presto-announce-done .4s ease 1.5s both;
753 }
754
755 /* The abilities that ran — named, because that is what the feature is. */
756 .presto-announce__chips {
757 display: flex;
758 flex-wrap: wrap;
759 gap: 6px;
760 margin: 0;
761 animation: presto-announce-done .4s ease 1.7s both;
762 }
763
764 .presto-announce__chips span {
765 padding: 3px 7px;
766 border-radius: 5px;
767 background: #ECFDF3;
768 font-family: var( --presto-mono );
769 font-size: 10px;
770 color: #067647;
771 }
772
773 .presto-announce__body {
774 padding: 24px 26px 22px;
775 }
776
777 /* Machine voice: version and feature name share the mono face. */
778 .presto-announce__tag {
779 display: flex;
780 align-items: center;
781 gap: 8px;
782 margin: 0 0 12px;
783 font-family: var( --presto-mono );
784 font-size: 10.5px;
785 color: var( --presto-muted );
786 }
787
788 .presto-announce__tag-new {
789 padding: 3px 7px;
790 border-radius: 4px;
791 background: rgba( 48, 88, 229, .09 );
792 color: var( --presto-blue );
793 font-weight: 600;
794 letter-spacing: .06em;
795 text-transform: uppercase;
796 }
797
798 .presto-announce__tag-name::before {
799 content: "\00b7";
800 margin-right: 8px;
801 opacity: .45;
802 }
803
804 .presto-announce__title {
805 margin: 0 0 9px;
806 font-size: 25px;
807 font-weight: 640;
808 line-height: 1.18;
809 letter-spacing: -.026em;
810 color: var( --presto-ink );
811 }
812
813 .presto-announce__text {
814 margin: 0 0 18px;
815 font-size: 13.5px;
816 line-height: 1.64;
817 color: var( --presto-muted );
818 }
819
820 /* Two features side by side, the way the settings screen lists them. */
821 .presto-announce__feats {
822 display: flex;
823 flex-wrap: wrap;
824 gap: 10px 26px;
825 margin: 0 0 20px;
826 padding: 0;
827 list-style: none;
828 }
829
830 .presto-announce__feats li {
831 display: flex;
832 align-items: center;
833 gap: 8px;
834 margin: 0;
835 font-size: 12.5px;
836 line-height: 1.4;
837 color: var( --presto-ink );
838 }
839
840 .presto-announce__feats svg {
841 flex: none;
842 color: var( --presto-blue );
843 }
844
845 /* CTA left, dismiss right. */
846 .presto-announce__actions {
847 display: flex;
848 align-items: center;
849 justify-content: space-between;
850 gap: 12px;
851 }
852
853 .presto-announce__close {
854 padding: 11px 2px;
855 border: 0;
856 background: none;
857 color: var( --presto-muted );
858 font-size: 13px;
859 line-height: 1;
860 cursor: pointer;
861 transition: color .16s ease;
862 }
863
864 .presto-announce__close:hover {
865 color: var( --presto-ink );
866 }
867
868 .presto-announce__cta {
869 display: inline-flex;
870 align-items: center;
871 gap: 7px;
872 padding: 11px 16px;
873 border: 0;
874 border-radius: 8px;
875 background: var( --presto-blue );
876 color: #fff;
877 font-size: 13px;
878 font-weight: 600;
879 line-height: 1;
880 text-decoration: none;
881 box-shadow: 0 6px 16px -8px rgba( 48, 88, 229, .7 );
882 transition: background-color .16s ease, box-shadow .16s ease, transform .16s ease;
883 }
884
885 .presto-announce__cta:hover,
886 .presto-announce__cta:focus {
887 background: #2246C9;
888 color: #fff;
889 box-shadow: 0 0 0 4px rgba( 48, 88, 229, .16 );
890 }
891
892 /* The arrow leans forward on hover — the only movement in the card. */
893 .presto-announce__cta:hover svg {
894 transform: translateX( 2px );
895 transition: transform .16s ease;
896 }
897
898 /* Slide paging, under a hairline so it reads as chrome, not content. */
899 .presto-announce__nav {
900 display: flex;
901 align-items: center;
902 justify-content: center;
903 gap: 14px;
904 padding: 11px 26px 15px;
905 border-top: 1px solid var( --presto-line );
906 }
907
908 .presto-announce__nav-btn {
909 display: flex;
910 align-items: center;
911 justify-content: center;
912 width: 28px;
913 height: 28px;
914 padding: 0;
915 border: 1px solid var( --presto-line );
916 border-radius: 50%;
917 background: #fff;
918 color: var( --presto-muted );
919 cursor: pointer;
920 transition: color .16s ease, border-color .16s ease;
921 }
922
923 .presto-announce__nav-btn:hover {
924 border-color: rgba( 16, 18, 35, .18 );
925 color: var( --presto-ink );
926 }
927
928 .presto-announce__nav-count {
929 margin: 0;
930 font-family: var( --presto-mono );
931 font-size: 11px;
932 color: var( --presto-muted );
933 }
934
935 .presto-announce__nav-count span {
936 color: var( --presto-ink );
937 }
938
939 /* Corner close, over the artwork. */
940 .presto-announce__x {
941 position: absolute;
942 top: 12px;
943 right: 12px;
944 z-index: 2;
945 display: flex;
946 align-items: center;
947 justify-content: center;
948 width: 28px;
949 height: 28px;
950 padding: 0;
951 border: 0;
952 border-radius: 50%;
953 background: rgba( 255, 255, 255, .12 );
954 color: #fff;
955 cursor: pointer;
956 transition: background-color .16s ease;
957 }
958
959 .presto-announce__x:hover {
960 background: rgba( 255, 255, 255, .24 );
961 color: #fff;
962 }
963
964 .presto-announce__x:focus-visible,
965 .presto-announce__cta:focus-visible,
966 .presto-announce__close:focus-visible,
967 .presto-announce__nav-btn:focus-visible {
968 outline: 2px solid var( --presto-blue );
969 outline-offset: 3px;
970 }
971
972 @keyframes presto-announce-veil {
973 from { opacity: 0; }
974 }
975
976 @keyframes presto-announce-rise {
977 from { opacity: 0; transform: translateY( 14px ) scale( .98 ); }
978 }
979
980 @keyframes presto-announce-chat {
981 from { opacity: 0; transform: translateY( 10px ) scale( .97 ); }
982 }
983
984 @keyframes presto-announce-done {
985 from { opacity: 0; }
986 }
987
988 @keyframes presto-announce-row {
989 from { background-color: transparent; }
990 }
991
992 @media ( max-width: 600px ) {
993 .presto-announce__art { padding: 20px 18px 18px; }
994 .presto-announce__chat { width: 100%; margin-top: 12px; }
995 .presto-announce__pill { gap: 7px; padding: 7px 10px; }
996 .presto-announce__pill-time { font-size: 9px; }
997 .presto-announce__body { padding: 20px 18px 18px; }
998 .presto-announce__title { font-size: 21px; }
999 .presto-announce__nav { padding: 10px 18px 14px; }
1000 }
1001
1002 @media ( prefers-reduced-motion: reduce ) {
1003 .presto-announce,
1004 .presto-announce__box,
1005 .presto-announce__slide,
1006 .presto-announce__pill,
1007 .presto-announce__chat,
1008 .presto-announce__chat-done,
1009 .presto-announce__chips,
1010 .presto-announce__list-new {
1011 animation: none;
1012 }
1013 }
1014 </style>
1015 <?php
1016 }
1017
1018 /**
1019 * Paging, focus containment and close behaviour.
1020 *
1021 * @return void
1022 */
1023 protected function script() {
1024 $args = array(
1025 'action' => 'presto_dismiss_announcement',
1026 '_wpnonce' => wp_create_nonce( self::NONCE_ACTION ),
1027 );
1028
1029 $js = sprintf(
1030 '( function() {
1031 var modal = document.querySelector( ".presto-announce" );
1032 if ( ! modal ) {
1033 return;
1034 }
1035
1036 var box = modal.querySelector( ".presto-announce__box" );
1037 var slides = modal.querySelectorAll( ".presto-announce__slide" );
1038 var count = modal.querySelector( ".presto-announce__nav-count span" );
1039 var at = 0;
1040
1041 function each( selector, callback ) {
1042 Array.prototype.forEach.call( modal.querySelectorAll( selector ), callback );
1043 }
1044
1045 function dismiss() {
1046 return window.fetch( %1$s, {
1047 method: "POST",
1048 credentials: "same-origin",
1049 body: new URLSearchParams( %2$s )
1050 } );
1051 }
1052
1053 function teardown() {
1054 // The modal renders on the block-editor screen, where Escape is
1055 // constant — a listener left bound after dismissal would fire an
1056 // admin-ajax POST on every later press.
1057 document.removeEventListener( "keydown", onKeydown );
1058 modal.remove();
1059 }
1060
1061 function close() {
1062 teardown();
1063 // A failed request only means the modal shows again.
1064 dismiss();
1065 }
1066
1067 // Wraps, so both slides are reachable from either arrow.
1068 function go( next ) {
1069 at = ( next + slides.length ) %% slides.length;
1070 for ( var i = 0; i < slides.length; i++ ) {
1071 slides[ i ].hidden = i !== at;
1072 }
1073 count.textContent = at + 1;
1074
1075 // Paging can hide whatever had focus, which drops activeElement
1076 // to <body> and puts it outside the trap. Only re-home when that
1077 // actually happened, so clicking an arrow keeps focus on it.
1078 var here = document.activeElement;
1079 if ( ! modal.contains( here ) || ( here.closest && here.closest( "[hidden]" ) ) ) {
1080 box.focus();
1081 }
1082 }
1083
1084 // Recomputed per keypress: the hidden slide keeps its buttons in the
1085 // DOM and they must stay out of the tab order.
1086 function tabbable() {
1087 return Array.prototype.filter.call(
1088 modal.querySelectorAll( "a[href], button" ),
1089 function( el ) {
1090 return ! el.closest( "[hidden]" );
1091 }
1092 );
1093 }
1094
1095 function onKeydown( event ) {
1096 if ( "Escape" === event.key ) {
1097 close();
1098 return;
1099 }
1100
1101 if ( "Tab" !== event.key ) {
1102 return;
1103 }
1104
1105 var items = tabbable();
1106 if ( ! items.length ) {
1107 return;
1108 }
1109
1110 var first = items[ 0 ];
1111 var last = items[ items.length - 1 ];
1112 var here = document.activeElement;
1113
1114 // Safari and Firefox do not focus a button on mouse click, so
1115 // activeElement is often <body> by now. Treat anything outside
1116 // the dialog as the wrap case, rather than letting the browser
1117 // tab away to the admin bar behind an aria-modal dialog.
1118 var inside = modal.contains( here ) && ! ( here.closest && here.closest( "[hidden]" ) );
1119
1120 if ( ! inside ) {
1121 event.preventDefault();
1122 ( event.shiftKey ? last : first ).focus();
1123 } else if ( event.shiftKey && ( here === first || here === box ) ) {
1124 event.preventDefault();
1125 last.focus();
1126 } else if ( ! event.shiftKey && here === last ) {
1127 event.preventDefault();
1128 first.focus();
1129 }
1130 }
1131
1132 // aria-modal is a promise that focus is in here, so move it — onto
1133 // the card itself, not the CTA, so nothing renders with a ring
1134 // around it before the user has touched anything.
1135 box.focus();
1136
1137 // Wait for the dismiss to land before leaving, otherwise the next
1138 // page renders the modal again before the request commits.
1139 each( ".presto-announce__cta", function( cta ) {
1140 cta.addEventListener( "click", function( event ) {
1141 event.preventDefault();
1142 var href = this.href;
1143 teardown();
1144 dismiss().catch( function() {} ).then( function() {
1145 window.location.assign( href );
1146 } );
1147 } );
1148 } );
1149
1150 // Closing on either slide dismisses the whole announcement.
1151 each( ".presto-announce__close, .presto-announce__x", function( button ) {
1152 button.addEventListener( "click", close );
1153 } );
1154
1155 each( "[data-presto-go]", function( button ) {
1156 button.addEventListener( "click", function() {
1157 go( at + Number( this.getAttribute( "data-presto-go" ) ) );
1158 } );
1159 } );
1160
1161 modal.addEventListener( "click", function( event ) {
1162 if ( event.target === modal ) {
1163 close();
1164 }
1165 } );
1166 document.addEventListener( "keydown", onKeydown );
1167 } )();',
1168 wp_json_encode( admin_url( 'admin-ajax.php' ) ),
1169 wp_json_encode( $args )
1170 );
1171
1172 wp_print_inline_script_tag( $js );
1173 }
1174 }
1175