API
1 week ago
Abilities
1 week ago
Blocks
3 weeks ago
License
1 month ago
OAuth
1 week 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 week ago
Learn.php
1 week ago
Menu.php
1 week ago
Migrations.php
2 years ago
NpsSurvey.php
6 months ago
Onboarding.php
2 months ago
Player.php
3 weeks ago
PluginInstaller.php
1 week ago
PreloadService.php
1 year ago
ProCompatibility.php
3 weeks ago
ReusableVideos.php
1 month ago
RewriteRulesManager.php
2 years ago
Scripts.php
1 month ago
Settings.php
3 months ago
Shortcodes.php
1 week ago
Streamer.php
2 months ago
Translation.php
3 weeks ago
Usage.php
1 week ago
VideoPostType.php
3 weeks ago
FeatureAnnounce.php
823 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 a major new feature after the user |
| 12 | * updates the plugin. Currently announces AI Abilities. |
| 13 | */ |
| 14 | class FeatureAnnounce { |
| 15 | |
| 16 | /** |
| 17 | * Bump this when announcing a different feature — a new id means everyone |
| 18 | * sees the modal again, even people who dismissed the previous one. |
| 19 | */ |
| 20 | const ANNOUNCEMENT_ID = 'wp_abilities_v1'; |
| 21 | |
| 22 | /** |
| 23 | * The release that introduced the feature being announced. Sites already on |
| 24 | * this version or newer never installed without it, so they don't need telling. |
| 25 | */ |
| 26 | const ANNOUNCED_IN_VERSION = '4.4.0'; |
| 27 | |
| 28 | /** |
| 29 | * Last plugin version this install has seen. Empty on a fresh install. |
| 30 | */ |
| 31 | const SEEN_VERSION_OPTION = 'presto_player_seen_version'; |
| 32 | |
| 33 | /** |
| 34 | * Announcement id waiting to be shown, set when the version changes. |
| 35 | */ |
| 36 | const PENDING_OPTION = 'presto_player_pending_announcement'; |
| 37 | |
| 38 | /** |
| 39 | * Announcement ids the current user has closed. |
| 40 | */ |
| 41 | const DISMISSED_META = 'presto_player_dismissed_announcements'; |
| 42 | |
| 43 | /** |
| 44 | * Nonce action for the dismiss request. |
| 45 | */ |
| 46 | const NONCE_ACTION = 'presto_dismiss_announcement'; |
| 47 | |
| 48 | /** |
| 49 | * Resolved plugin version, cached for the request. |
| 50 | * |
| 51 | * @var string|null |
| 52 | */ |
| 53 | protected $version = null; |
| 54 | |
| 55 | /** |
| 56 | * Register hooks. |
| 57 | * |
| 58 | * @return void |
| 59 | */ |
| 60 | public function register() { |
| 61 | add_action( 'admin_init', array( $this, 'trackVersion' ) ); |
| 62 | add_action( 'admin_footer', array( $this, 'render' ) ); |
| 63 | add_action( 'wp_ajax_presto_dismiss_announcement', array( $this, 'dismiss' ) ); |
| 64 | } |
| 65 | |
| 66 | /** |
| 67 | * Queue the announcement when the running version changes. |
| 68 | * |
| 69 | * @return void |
| 70 | */ |
| 71 | public function trackVersion() { |
| 72 | $seen = get_option( self::SEEN_VERSION_OPTION, '' ); |
| 73 | if ( $this->version() === $seen ) { |
| 74 | return; |
| 75 | } |
| 76 | |
| 77 | // No recorded version means either a brand new install or the first load |
| 78 | // after updating into the release that added this class. |
| 79 | // onboarding_completed tells the two apart — it is backfilled for |
| 80 | // pre-4.2.0 installs, so an established site always has it set. |
| 81 | // Bail before stamping: a site whose backfill missed it would otherwise be |
| 82 | // marked as caught up and could never be told about the feature. |
| 83 | if ( '' === $seen && 'yes' !== get_option( 'presto_player_onboarding_completed', 'no' ) ) { |
| 84 | return; |
| 85 | } |
| 86 | |
| 87 | update_option( self::SEEN_VERSION_OPTION, $this->version() ); |
| 88 | |
| 89 | // Only announce to sites that were here before the feature shipped. Without |
| 90 | // this floor, a site that installs 4.5 in 2027 and updates to 4.5.1 gets a |
| 91 | // full-screen "New in Presto Player" for something that predates it. |
| 92 | if ( '' !== $seen && version_compare( $seen, self::ANNOUNCED_IN_VERSION, '>=' ) ) { |
| 93 | return; |
| 94 | } |
| 95 | |
| 96 | update_option( self::PENDING_OPTION, self::ANNOUNCEMENT_ID ); |
| 97 | } |
| 98 | |
| 99 | /** |
| 100 | * Whether the modal should be printed for this request. |
| 101 | * |
| 102 | * @return bool |
| 103 | */ |
| 104 | protected function shouldShow() { |
| 105 | // manage_options, not the menu's publish_posts: the settings REST route |
| 106 | // rejects anyone lower, so an author following the CTA just lands on a |
| 107 | // "Couldn't load settings" page. |
| 108 | if ( ! current_user_can( 'manage_options' ) ) { |
| 109 | return false; |
| 110 | } |
| 111 | |
| 112 | // Nothing to announce on WordPress versions without the Abilities API. |
| 113 | if ( ! function_exists( 'wp_register_ability' ) ) { |
| 114 | return false; |
| 115 | } |
| 116 | |
| 117 | // Only after an update queued it — see trackVersion(). |
| 118 | if ( self::ANNOUNCEMENT_ID !== get_option( self::PENDING_OPTION, '' ) ) { |
| 119 | return false; |
| 120 | } |
| 121 | |
| 122 | if ( ! $this->onPrestoScreen() ) { |
| 123 | return false; |
| 124 | } |
| 125 | |
| 126 | return ! in_array( self::ANNOUNCEMENT_ID, $this->dismissed(), true ); |
| 127 | } |
| 128 | |
| 129 | /** |
| 130 | * Whether we are on one of Presto Player's own admin screens. |
| 131 | * |
| 132 | * The announcement belongs inside the plugin — it should not follow someone |
| 133 | * around the rest of WordPress. |
| 134 | * |
| 135 | * @return bool |
| 136 | */ |
| 137 | protected function onPrestoScreen() { |
| 138 | if ( ! function_exists( 'get_current_screen' ) ) { |
| 139 | return false; |
| 140 | } |
| 141 | |
| 142 | $screen = get_current_screen(); |
| 143 | if ( ! $screen ) { |
| 144 | return false; |
| 145 | } |
| 146 | |
| 147 | // Media Hub list and editor. |
| 148 | if ( 'pp_video_block' === $screen->post_type ) { |
| 149 | return true; |
| 150 | } |
| 151 | |
| 152 | // Every menu page hangs off the presto-dashboard slug, so the screen id |
| 153 | // carries it — toplevel_page_presto-dashboard and friends. |
| 154 | return false !== strpos( $screen->id, 'presto' ); |
| 155 | } |
| 156 | |
| 157 | /** |
| 158 | * Print the modal. |
| 159 | * |
| 160 | * @return void |
| 161 | */ |
| 162 | public function render() { |
| 163 | if ( ! $this->shouldShow() ) { |
| 164 | return; |
| 165 | } |
| 166 | |
| 167 | $settings_url = admin_url( 'admin.php?page=presto-dashboard&tab=Settings§ion=mcp' ); |
| 168 | |
| 169 | $feats = array( |
| 170 | array( |
| 171 | 'icon' => 'wand', |
| 172 | 'label' => __( 'Create a video from a link', 'presto-player' ), |
| 173 | ), |
| 174 | array( |
| 175 | 'icon' => 'chart', |
| 176 | 'label' => __( 'Ask for your analytics', 'presto-player' ), |
| 177 | ), |
| 178 | array( |
| 179 | 'icon' => 'shield', |
| 180 | 'label' => __( 'Read-only until you allow more', 'presto-player' ), |
| 181 | ), |
| 182 | ); |
| 183 | |
| 184 | ?> |
| 185 | <div class="presto-announce" role="dialog" aria-modal="true" aria-labelledby="presto-announce-title"> |
| 186 | <div class="presto-announce__box" tabindex="-1"> |
| 187 | <button type="button" class="presto-announce__x" aria-label="<?php esc_attr_e( 'Close', 'presto-player' ); ?>"> |
| 188 | <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> |
| 189 | </button> |
| 190 | |
| 191 | <?php $this->art(); ?> |
| 192 | |
| 193 | <div class="presto-announce__body"> |
| 194 | <p class="presto-announce__tag"> |
| 195 | <span class="presto-announce__tag-new"> |
| 196 | <?php esc_html_e( 'New in Presto Player', 'presto-player' ); ?> |
| 197 | </span> |
| 198 | <span class="presto-announce__tag-ver"><?php echo esc_html( $this->shortVersion() ); ?></span> |
| 199 | </p> |
| 200 | |
| 201 | <h2 class="presto-announce__title" id="presto-announce-title"> |
| 202 | <?php esc_html_e( 'Talk to your video library', 'presto-player' ); ?> |
| 203 | </h2> |
| 204 | |
| 205 | <p class="presto-announce__text"> |
| 206 | <?php esc_html_e( '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' ); ?> |
| 207 | </p> |
| 208 | |
| 209 | <ul class="presto-announce__feats"> |
| 210 | <?php foreach ( $feats as $feat ) : ?> |
| 211 | <li> |
| 212 | <?php $this->icon( $feat['icon'] ); ?> |
| 213 | <span><?php echo esc_html( $feat['label'] ); ?></span> |
| 214 | </li> |
| 215 | <?php endforeach; ?> |
| 216 | </ul> |
| 217 | |
| 218 | <div class="presto-announce__actions"> |
| 219 | <a class="presto-announce__cta" href="<?php echo esc_url( $settings_url ); ?>"> |
| 220 | <?php esc_html_e( 'Set up AI Abilities', 'presto-player' ); ?> |
| 221 | <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> |
| 222 | </a> |
| 223 | <button type="button" class="presto-announce__close"> |
| 224 | <?php esc_html_e( 'Not now', 'presto-player' ); ?> |
| 225 | </button> |
| 226 | </div> |
| 227 | </div> |
| 228 | </div> |
| 229 | </div> |
| 230 | <?php |
| 231 | |
| 232 | $this->styles(); |
| 233 | $this->script(); |
| 234 | } |
| 235 | |
| 236 | /** |
| 237 | * Small line icon for a feature row. |
| 238 | * |
| 239 | * @param string $name Icon key. |
| 240 | * @return void |
| 241 | */ |
| 242 | protected function icon( $name ) { |
| 243 | $paths = array( |
| 244 | '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"/>', |
| 245 | 'chart' => '<path d="M4 16V9m4 7V5m4 11v-5m4 5V8"/>', |
| 246 | '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"/>', |
| 247 | ); |
| 248 | |
| 249 | if ( empty( $paths[ $name ] ) ) { |
| 250 | return; |
| 251 | } |
| 252 | |
| 253 | printf( |
| 254 | '<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>', |
| 255 | $paths[ $name ] // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static markup from the map above. |
| 256 | ); |
| 257 | } |
| 258 | |
| 259 | /** |
| 260 | * Store the dismissal against the current user. |
| 261 | * |
| 262 | * @return void |
| 263 | */ |
| 264 | public function dismiss() { |
| 265 | $nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : ''; |
| 266 | if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) { |
| 267 | wp_send_json_error(); |
| 268 | } |
| 269 | |
| 270 | if ( ! current_user_can( 'manage_options' ) ) { |
| 271 | wp_send_json_error(); |
| 272 | } |
| 273 | |
| 274 | $dismissed = $this->dismissed(); |
| 275 | if ( ! in_array( self::ANNOUNCEMENT_ID, $dismissed, true ) ) { |
| 276 | $dismissed[] = self::ANNOUNCEMENT_ID; |
| 277 | update_user_meta( get_current_user_id(), self::DISMISSED_META, $dismissed ); |
| 278 | } |
| 279 | |
| 280 | wp_send_json_success(); |
| 281 | } |
| 282 | |
| 283 | /** |
| 284 | * Announcement ids this user has already closed. |
| 285 | * |
| 286 | * @return string[] |
| 287 | */ |
| 288 | protected function dismissed() { |
| 289 | $dismissed = get_user_meta( get_current_user_id(), self::DISMISSED_META, true ); |
| 290 | return is_array( $dismissed ) ? $dismissed : array(); |
| 291 | } |
| 292 | |
| 293 | /** |
| 294 | * Full plugin version. There is no version constant in the free plugin, so |
| 295 | * fall back to the plugin header the same way the OAuth endpoints do. |
| 296 | * |
| 297 | * @return string |
| 298 | */ |
| 299 | protected function version() { |
| 300 | if ( null !== $this->version ) { |
| 301 | return $this->version; |
| 302 | } |
| 303 | |
| 304 | if ( defined( 'PRESTO_PLAYER_VERSION' ) ) { |
| 305 | $this->version = (string) constant( 'PRESTO_PLAYER_VERSION' ); |
| 306 | return $this->version; |
| 307 | } |
| 308 | |
| 309 | if ( ! function_exists( 'get_plugin_data' ) ) { |
| 310 | require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
| 311 | } |
| 312 | |
| 313 | $data = get_plugin_data( PRESTO_PLAYER_PLUGIN_FILE, false, false ); |
| 314 | $this->version = empty( $data['Version'] ) ? '' : (string) $data['Version']; |
| 315 | |
| 316 | return $this->version; |
| 317 | } |
| 318 | |
| 319 | /** |
| 320 | * Version without the patch part, for display — 4.3.2 becomes 4.3. |
| 321 | * |
| 322 | * @return string |
| 323 | */ |
| 324 | protected function shortVersion() { |
| 325 | $parts = explode( '.', $this->version() ); |
| 326 | return implode( '.', array_slice( $parts, 0, 2 ) ); |
| 327 | } |
| 328 | |
| 329 | /** |
| 330 | * Illustration: the Media Hub list with an assistant adding a video to it. |
| 331 | * |
| 332 | * Sample content is intentionally not translatable. It is a product mock |
| 333 | * behind aria-hidden — the same as a screenshot — so translating it would |
| 334 | * only add contextless strings to the .pot file. |
| 335 | * |
| 336 | * @return void |
| 337 | */ |
| 338 | protected function art() { |
| 339 | ?> |
| 340 | <div class="presto-announce__art" aria-hidden="true"> |
| 341 | <ul class="presto-announce__list"> |
| 342 | <li> |
| 343 | <span>Onboarding · Lesson 1</span> |
| 344 | <em>YouTube · 4:21</em> |
| 345 | </li> |
| 346 | <li> |
| 347 | <span>Onboarding · Lesson 2</span> |
| 348 | <em>Bunny · 12:04</em> |
| 349 | </li> |
| 350 | <li class="presto-announce__list-new"> |
| 351 | <span>Product Demo 2026</span> |
| 352 | <em>YouTube · 3:38</em> |
| 353 | </li> |
| 354 | </ul> |
| 355 | |
| 356 | <div class="presto-announce__chat"> |
| 357 | <p class="presto-announce__chat-who">Claude</p> |
| 358 | <p class="presto-announce__chat-ask">Add this video as “Product Demo 2026” and give me the shortcode.</p> |
| 359 | <p class="presto-announce__chat-done">Done — [presto_player id=4182]</p> |
| 360 | <p class="presto-announce__chips"> |
| 361 | <span>create-video-youtube</span> |
| 362 | <span>get-video-shortcode</span> |
| 363 | </p> |
| 364 | </div> |
| 365 | </div> |
| 366 | <?php |
| 367 | } |
| 368 | |
| 369 | /** |
| 370 | * Modal styles. Printed inline because this only renders once per user. |
| 371 | * |
| 372 | * @return void |
| 373 | */ |
| 374 | protected function styles() { |
| 375 | ?> |
| 376 | <style> |
| 377 | .presto-announce { |
| 378 | --presto-ink: #101223; |
| 379 | --presto-muted: #656D88; |
| 380 | --presto-line: rgba( 16, 18, 35, .07 ); |
| 381 | --presto-blue: #3058E5; |
| 382 | --presto-violet: #9A20F8; |
| 383 | --presto-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; |
| 384 | |
| 385 | position: fixed; |
| 386 | inset: 0; |
| 387 | z-index: 160000; |
| 388 | display: flex; |
| 389 | align-items: center; |
| 390 | justify-content: center; |
| 391 | padding: 24px; |
| 392 | /* Scroll the overlay, never the card — a scrollbar inside the panel |
| 393 | looks like a mistake. */ |
| 394 | overflow-y: auto; |
| 395 | background: rgba( 16, 18, 35, .46 ); |
| 396 | animation: presto-announce-veil .22s ease both; |
| 397 | } |
| 398 | |
| 399 | .presto-announce__box { |
| 400 | position: relative; |
| 401 | overflow: hidden; |
| 402 | width: 100%; |
| 403 | max-width: 520px; |
| 404 | /* Centres while allowing the overlay to scroll instead of clipping. */ |
| 405 | margin: auto; |
| 406 | border-radius: 16px; |
| 407 | background: #fff; |
| 408 | /* Layered rather than one big blur — reads lifted, not stamped on. */ |
| 409 | box-shadow: |
| 410 | 0 1px 2px rgba( 16, 18, 35, .05 ), |
| 411 | 0 12px 24px -10px rgba( 16, 18, 35, .14 ), |
| 412 | 0 44px 72px -32px rgba( 16, 18, 35, .34 ); |
| 413 | animation: presto-announce-rise .34s cubic-bezier( .2, .8, .2, 1 ) both; |
| 414 | } |
| 415 | |
| 416 | /* Holds focus for aria-modal without drawing a ring — it's a container, |
| 417 | not something you interact with. */ |
| 418 | .presto-announce__box:focus { |
| 419 | outline: none; |
| 420 | } |
| 421 | |
| 422 | /* The only brand wash: a 2px edge, flush to the top. */ |
| 423 | .presto-announce__box::before { |
| 424 | content: ""; |
| 425 | position: absolute; |
| 426 | top: 0; |
| 427 | left: 0; |
| 428 | right: 0; |
| 429 | height: 2px; |
| 430 | background: linear-gradient( 90deg, var( --presto-violet ), #5B37F0 52%, var( --presto-blue ) ); |
| 431 | } |
| 432 | |
| 433 | /* A product shot in markup, matching the landing page: the library list |
| 434 | with an assistant working against it. Deliberately dark so the graphics |
| 435 | read as a separate section from the content below. */ |
| 436 | .presto-announce__art { |
| 437 | position: relative; |
| 438 | padding: 28px 26px 26px; |
| 439 | background: |
| 440 | radial-gradient( 82% 122% at 4% -14%, rgba( 154, 32, 248, .42 ), transparent 62% ), |
| 441 | radial-gradient( 70% 108% at 99% -6%, rgba( 48, 88, 229, .4 ), transparent 60% ), |
| 442 | #0D0F1E; |
| 443 | } |
| 444 | |
| 445 | .presto-announce__list { |
| 446 | margin: 0; |
| 447 | padding: 6px; |
| 448 | border-radius: 10px; |
| 449 | background: #fff; |
| 450 | box-shadow: 0 12px 30px -14px rgba( 5, 6, 15, .75 ); |
| 451 | list-style: none; |
| 452 | } |
| 453 | |
| 454 | .presto-announce__list li { |
| 455 | display: flex; |
| 456 | align-items: center; |
| 457 | justify-content: space-between; |
| 458 | gap: 12px; |
| 459 | margin: 0; |
| 460 | padding: 9px 10px; |
| 461 | border-radius: 7px; |
| 462 | font-size: 12.5px; |
| 463 | color: var( --presto-ink ); |
| 464 | } |
| 465 | |
| 466 | .presto-announce__list em { |
| 467 | flex: none; |
| 468 | font-family: var( --presto-mono ); |
| 469 | font-size: 10.5px; |
| 470 | font-style: normal; |
| 471 | color: var( --presto-muted ); |
| 472 | } |
| 473 | |
| 474 | /* The row the assistant just created. */ |
| 475 | .presto-announce__list-new { |
| 476 | background: rgba( 48, 88, 229, .07 ); |
| 477 | font-weight: 600; |
| 478 | animation: presto-announce-row .45s ease 1.5s both; |
| 479 | } |
| 480 | |
| 481 | .presto-announce__chat { |
| 482 | position: relative; |
| 483 | width: 78%; |
| 484 | margin: -16px 0 0 auto; |
| 485 | padding: 13px 14px; |
| 486 | border-radius: 11px; |
| 487 | background: #fff; |
| 488 | box-shadow: 0 16px 36px -14px rgba( 5, 6, 15, .8 ); |
| 489 | animation: presto-announce-chat .45s cubic-bezier( .2, .8, .2, 1 ) .5s both; |
| 490 | } |
| 491 | |
| 492 | .presto-announce__chat-who { |
| 493 | display: flex; |
| 494 | align-items: center; |
| 495 | gap: 7px; |
| 496 | margin: 0 0 9px; |
| 497 | font-size: 11.5px; |
| 498 | font-weight: 600; |
| 499 | color: var( --presto-ink ); |
| 500 | } |
| 501 | |
| 502 | .presto-announce__chat-who::before { |
| 503 | content: ""; |
| 504 | width: 16px; |
| 505 | height: 16px; |
| 506 | border-radius: 5px; |
| 507 | background: linear-gradient( 135deg, var( --presto-violet ), var( --presto-blue ) ); |
| 508 | } |
| 509 | |
| 510 | .presto-announce__chat-ask { |
| 511 | margin: 0 0 8px; |
| 512 | padding: 8px 10px; |
| 513 | border-radius: 7px; |
| 514 | background: rgba( 16, 18, 35, .04 ); |
| 515 | font-size: 11.5px; |
| 516 | line-height: 1.5; |
| 517 | color: var( --presto-ink ); |
| 518 | } |
| 519 | |
| 520 | .presto-announce__chat-done { |
| 521 | margin: 0 0 9px; |
| 522 | font-family: var( --presto-mono ); |
| 523 | font-size: 10.5px; |
| 524 | line-height: 1.5; |
| 525 | color: var( --presto-muted ); |
| 526 | animation: presto-announce-done .4s ease 1.5s both; |
| 527 | } |
| 528 | |
| 529 | /* The abilities that ran — named, because that is what the feature is. */ |
| 530 | .presto-announce__chips { |
| 531 | display: flex; |
| 532 | flex-wrap: wrap; |
| 533 | gap: 6px; |
| 534 | margin: 0; |
| 535 | animation: presto-announce-done .4s ease 1.7s both; |
| 536 | } |
| 537 | |
| 538 | .presto-announce__chips span { |
| 539 | padding: 3px 7px; |
| 540 | border-radius: 5px; |
| 541 | background: #ECFDF3; |
| 542 | font-family: var( --presto-mono ); |
| 543 | font-size: 10px; |
| 544 | color: #067647; |
| 545 | } |
| 546 | |
| 547 | .presto-announce__body { |
| 548 | padding: 24px 26px 22px; |
| 549 | } |
| 550 | |
| 551 | /* Machine voice: version and feature name share the mono face. */ |
| 552 | .presto-announce__tag { |
| 553 | display: flex; |
| 554 | align-items: center; |
| 555 | gap: 8px; |
| 556 | margin: 0 0 12px; |
| 557 | font-family: var( --presto-mono ); |
| 558 | font-size: 10.5px; |
| 559 | color: var( --presto-muted ); |
| 560 | } |
| 561 | |
| 562 | .presto-announce__tag-new { |
| 563 | padding: 3px 7px; |
| 564 | border-radius: 4px; |
| 565 | background: rgba( 48, 88, 229, .09 ); |
| 566 | color: var( --presto-blue ); |
| 567 | font-weight: 600; |
| 568 | letter-spacing: .06em; |
| 569 | text-transform: uppercase; |
| 570 | } |
| 571 | |
| 572 | .presto-announce__tag-name::before { |
| 573 | content: "\00b7"; |
| 574 | margin-right: 8px; |
| 575 | opacity: .45; |
| 576 | } |
| 577 | |
| 578 | .presto-announce__title { |
| 579 | margin: 0 0 9px; |
| 580 | font-size: 25px; |
| 581 | font-weight: 640; |
| 582 | line-height: 1.18; |
| 583 | letter-spacing: -.026em; |
| 584 | color: var( --presto-ink ); |
| 585 | } |
| 586 | |
| 587 | .presto-announce__text { |
| 588 | margin: 0 0 18px; |
| 589 | font-size: 13.5px; |
| 590 | line-height: 1.64; |
| 591 | color: var( --presto-muted ); |
| 592 | } |
| 593 | |
| 594 | /* Two features side by side, the way the settings screen lists them. */ |
| 595 | .presto-announce__feats { |
| 596 | display: flex; |
| 597 | flex-wrap: wrap; |
| 598 | gap: 10px 26px; |
| 599 | margin: 0 0 20px; |
| 600 | padding: 0; |
| 601 | list-style: none; |
| 602 | } |
| 603 | |
| 604 | .presto-announce__feats li { |
| 605 | display: flex; |
| 606 | align-items: center; |
| 607 | gap: 8px; |
| 608 | margin: 0; |
| 609 | font-size: 12.5px; |
| 610 | line-height: 1.4; |
| 611 | color: var( --presto-ink ); |
| 612 | } |
| 613 | |
| 614 | .presto-announce__feats svg { |
| 615 | flex: none; |
| 616 | color: var( --presto-blue ); |
| 617 | } |
| 618 | |
| 619 | /* CTA left, dismiss right. */ |
| 620 | .presto-announce__actions { |
| 621 | display: flex; |
| 622 | align-items: center; |
| 623 | justify-content: space-between; |
| 624 | gap: 12px; |
| 625 | } |
| 626 | |
| 627 | .presto-announce__close { |
| 628 | padding: 11px 2px; |
| 629 | border: 0; |
| 630 | background: none; |
| 631 | color: var( --presto-muted ); |
| 632 | font-size: 13px; |
| 633 | line-height: 1; |
| 634 | cursor: pointer; |
| 635 | transition: color .16s ease; |
| 636 | } |
| 637 | |
| 638 | .presto-announce__close:hover { |
| 639 | color: var( --presto-ink ); |
| 640 | } |
| 641 | |
| 642 | .presto-announce__cta { |
| 643 | display: inline-flex; |
| 644 | align-items: center; |
| 645 | gap: 7px; |
| 646 | padding: 11px 16px; |
| 647 | border: 0; |
| 648 | border-radius: 8px; |
| 649 | background: var( --presto-blue ); |
| 650 | color: #fff; |
| 651 | font-size: 13px; |
| 652 | font-weight: 600; |
| 653 | line-height: 1; |
| 654 | text-decoration: none; |
| 655 | box-shadow: 0 6px 16px -8px rgba( 48, 88, 229, .7 ); |
| 656 | transition: background-color .16s ease, box-shadow .16s ease, transform .16s ease; |
| 657 | } |
| 658 | |
| 659 | .presto-announce__cta:hover, |
| 660 | .presto-announce__cta:focus { |
| 661 | background: #2246C9; |
| 662 | color: #fff; |
| 663 | box-shadow: 0 0 0 4px rgba( 48, 88, 229, .16 ); |
| 664 | } |
| 665 | |
| 666 | /* The arrow leans forward on hover — the only movement in the card. */ |
| 667 | .presto-announce__cta:hover svg { |
| 668 | transform: translateX( 2px ); |
| 669 | transition: transform .16s ease; |
| 670 | } |
| 671 | |
| 672 | /* Corner close, over the artwork. */ |
| 673 | .presto-announce__x { |
| 674 | position: absolute; |
| 675 | top: 12px; |
| 676 | right: 12px; |
| 677 | z-index: 2; |
| 678 | display: flex; |
| 679 | align-items: center; |
| 680 | justify-content: center; |
| 681 | width: 28px; |
| 682 | height: 28px; |
| 683 | padding: 0; |
| 684 | border: 0; |
| 685 | border-radius: 50%; |
| 686 | background: rgba( 255, 255, 255, .12 ); |
| 687 | color: #fff; |
| 688 | cursor: pointer; |
| 689 | transition: background-color .16s ease; |
| 690 | } |
| 691 | |
| 692 | .presto-announce__x:hover { |
| 693 | background: rgba( 255, 255, 255, .24 ); |
| 694 | color: #fff; |
| 695 | } |
| 696 | |
| 697 | .presto-announce__x:focus-visible, |
| 698 | .presto-announce__cta:focus-visible, |
| 699 | .presto-announce__close:focus-visible { |
| 700 | outline: 2px solid var( --presto-blue ); |
| 701 | outline-offset: 3px; |
| 702 | } |
| 703 | |
| 704 | @keyframes presto-announce-veil { |
| 705 | from { opacity: 0; } |
| 706 | } |
| 707 | |
| 708 | @keyframes presto-announce-rise { |
| 709 | from { opacity: 0; transform: translateY( 14px ) scale( .98 ); } |
| 710 | } |
| 711 | |
| 712 | @keyframes presto-announce-chat { |
| 713 | from { opacity: 0; transform: translateY( 10px ) scale( .97 ); } |
| 714 | } |
| 715 | |
| 716 | @keyframes presto-announce-done { |
| 717 | from { opacity: 0; } |
| 718 | } |
| 719 | |
| 720 | @keyframes presto-announce-row { |
| 721 | from { background-color: transparent; } |
| 722 | } |
| 723 | |
| 724 | @media ( max-width: 600px ) { |
| 725 | .presto-announce__art { padding: 20px 18px 18px; } |
| 726 | .presto-announce__chat { width: 100%; margin-top: 12px; } |
| 727 | .presto-announce__body { padding: 20px 18px 18px; } |
| 728 | .presto-announce__title { font-size: 21px; } |
| 729 | } |
| 730 | |
| 731 | @media ( prefers-reduced-motion: reduce ) { |
| 732 | .presto-announce, |
| 733 | .presto-announce__box, |
| 734 | .presto-announce__chat, |
| 735 | .presto-announce__chat-done, |
| 736 | .presto-announce__chips, |
| 737 | .presto-announce__list-new { |
| 738 | animation: none; |
| 739 | } |
| 740 | } |
| 741 | </style> |
| 742 | <?php |
| 743 | } |
| 744 | |
| 745 | /** |
| 746 | * Close behaviour. |
| 747 | * |
| 748 | * @return void |
| 749 | */ |
| 750 | protected function script() { |
| 751 | $args = array( |
| 752 | 'action' => 'presto_dismiss_announcement', |
| 753 | '_wpnonce' => wp_create_nonce( self::NONCE_ACTION ), |
| 754 | ); |
| 755 | |
| 756 | $js = sprintf( |
| 757 | '( function() { |
| 758 | var modal = document.querySelector( ".presto-announce" ); |
| 759 | if ( ! modal ) { |
| 760 | return; |
| 761 | } |
| 762 | |
| 763 | function dismiss() { |
| 764 | return window.fetch( %1$s, { |
| 765 | method: "POST", |
| 766 | credentials: "same-origin", |
| 767 | body: new URLSearchParams( %2$s ) |
| 768 | } ); |
| 769 | } |
| 770 | |
| 771 | function close() { |
| 772 | // The modal renders on the block-editor screen, where Escape is |
| 773 | // constant — a listener left bound after dismissal would fire an |
| 774 | // admin-ajax POST on every later press. |
| 775 | document.removeEventListener( "keydown", onKeydown ); |
| 776 | modal.remove(); |
| 777 | // A failed request only means the modal shows again. |
| 778 | dismiss(); |
| 779 | } |
| 780 | |
| 781 | function onKeydown( event ) { |
| 782 | if ( "Escape" === event.key ) { |
| 783 | close(); |
| 784 | } |
| 785 | } |
| 786 | |
| 787 | // aria-modal is a promise that focus is in here, so move it — onto |
| 788 | // the card itself, not the CTA, so nothing renders with a ring |
| 789 | // around it before the user has touched anything. |
| 790 | modal.querySelector( ".presto-announce__box" ).focus(); |
| 791 | |
| 792 | var cta = modal.querySelector( ".presto-announce__cta" ); |
| 793 | |
| 794 | // Wait for the dismiss to land before leaving, otherwise the next |
| 795 | // page renders the modal again before the request commits. |
| 796 | cta.addEventListener( "click", function( event ) { |
| 797 | event.preventDefault(); |
| 798 | var href = this.href; |
| 799 | document.removeEventListener( "keydown", onKeydown ); |
| 800 | modal.remove(); |
| 801 | dismiss().catch( function() {} ).then( function() { |
| 802 | window.location.assign( href ); |
| 803 | } ); |
| 804 | } ); |
| 805 | |
| 806 | modal.querySelector( ".presto-announce__close" ).addEventListener( "click", close ); |
| 807 | modal.querySelector( ".presto-announce__x" ).addEventListener( "click", close ); |
| 808 | |
| 809 | modal.addEventListener( "click", function( event ) { |
| 810 | if ( event.target === modal ) { |
| 811 | close(); |
| 812 | } |
| 813 | } ); |
| 814 | document.addEventListener( "keydown", onKeydown ); |
| 815 | } )();', |
| 816 | wp_json_encode( admin_url( 'admin-ajax.php' ) ), |
| 817 | wp_json_encode( $args ) |
| 818 | ); |
| 819 | |
| 820 | wp_print_inline_script_tag( $js ); |
| 821 | } |
| 822 | } |
| 823 |