` shell components here because they only ship inside the * desktop bundle, which is precisely *not* loaded on the classic admin * screens where this dialog is allowed to appear. * * @package OpenStation */ defined( 'ABSPATH' ) || exit; /** Slug stored in `desktop_mode_seen_intros` for this dialog. */ const OPENSTATION_WELCOME_INTRO_SLUG = 'activation-welcome'; /** * Decides whether the welcome dialog should render on the current request. * * Six gates: * * 1. We're inside `/wp-admin` (`is_admin()`). * 2. The user is logged in and can `read` (sanity gate — the dialog has * no destructive surface, but anonymous output makes no sense). * 3. The request is NOT chromeless — chromeless pages are iframes * rendering inside the desktop shell; the parent shell already shows * its own UX. * 4. OpenStation is NOT already enabled for the user. This is a * "switch to OpenStation" promo, so it has nothing to say once the * user is in the shell. The desktop shell's *parent* page is admin * context and is not chromeless, so without this gate the dialog * re-renders there the moment the user clicks "Switch to * OpenStation", which reads as a duplicate dialog because the * fire-and-forget seen-intro POST races the redirect into the shell * and often loses. * 5. The user has not already dismissed this intro. * 6. The `openstation_show_welcome_dialog` filter returns truthy, so * sites can suppress the dialog entirely (e.g. managed-host onboarding * flows that ship their own). * * @return bool */ function openstation_should_show_welcome_dialog() { if ( ! is_admin() || ! is_user_logged_in() ) { return false; } if ( ! current_user_can( 'read' ) ) { return false; } if ( function_exists( 'openstation_is_chromeless_request' ) && openstation_is_chromeless_request() ) { return false; } if ( function_exists( 'openstation_is_enabled' ) && openstation_is_enabled() ) { return false; } $user_id = get_current_user_id(); if ( openstation_has_seen_intro( $user_id, OPENSTATION_WELCOME_INTRO_SLUG ) ) { return false; } /** * Filters whether the first-run welcome dialog should render for * the current user on the current request. All earlier gates * (admin context, capability, chromeless, seen-state) have * already passed by the time this filter fires. * * @param bool $show Whether to render the dialog. Default true. * @param int $user_id Current user ID. */ return (bool) apply_filters( 'openstation_show_welcome_dialog', true, $user_id ); } /** * Returns one of OpenStation's own icons as inline SVG markup. * * Reads the outlined copies in `assets/icons/` (the ones registered with * Core's icon registry), which paint with `currentColor`, so the dialog's * CSS decides their colour. Returns an empty string for a missing file, * which leaves an empty icon slot rather than breaking the dialog. * * @param string $slug Icon slug, e.g. `windows`. * @return string Sanitised SVG markup. */ function openstation_welcome_dialog_icon( $slug ) { $path = OPENSTATION_DIR . 'assets/icons/' . sanitize_key( $slug ) . '.svg'; if ( ! is_readable( $path ) ) { return ''; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin file, not a remote URL. $svg = (string) file_get_contents( $path ); return wp_kses( $svg, array( 'svg' => array( 'xmlns' => true, 'viewbox' => true, 'width' => true, 'height' => true, 'aria-hidden' => true, 'focusable' => true, ), 'path' => array( 'd' => true, 'fill' => true, ), ) ); } /** * Prints the welcome dialog markup, styles and dismiss script into * `admin_footer`. * * Self-contained on purpose: everything is scoped under the * `.os-welcome` namespace so it cannot collide with the host * admin theme. The dismiss button POSTs to the seen-intros REST route, * which is exactly the same endpoint the in-shell intros use. * * The look is deliberately quiet: a light card with a dark panel that * shows two OpenStation windows, the holo mark, and Pulse kept to the * feature icons. Every colour is a literal from the brand palette rather * than a `--os-*` token, because `variables.css` is not loaded in classic * admin. Spacing sits on an 8px grid. */ function openstation_render_welcome_dialog() { if ( ! openstation_should_show_welcome_dialog() ) { return; } $rest_url = esc_url_raw( rest_url( 'desktop-mode/v1/intros/seen' ) ); $rest_nonce = wp_create_nonce( 'wp_rest' ); $ajax_url = esc_url_raw( admin_url( 'admin-ajax.php' ) ); $ajax_nonce = wp_create_nonce( 'save-openstation' ); $slug = OPENSTATION_WELCOME_INTRO_SLUG; $font_url = OPENSTATION_URL . 'assets/fonts/Geist-Variable.woff2'; $mono_url = OPENSTATION_URL . 'assets/fonts/GeistMono-Variable.woff2'; $mark_url = OPENSTATION_URL . 'assets/images/openstation-mark-holo.svg'; // All user-facing strings are passed through translation; the dialog // is keyboard-dismissible (Escape) and moves initial focus to the // primary CTA. $title = __( 'Welcome to OpenStation', 'desktop-mode' ); $body = __( 'Your admin, as a desktop. Keep several screens open at once and move between tasks without losing your place.', 'desktop-mode' ); $later = __( 'Not now', 'desktop-mode' ); $enable = __( 'Switch to OpenStation', 'desktop-mode' ); $enabling = __( 'Switching…', 'desktop-mode' ); $features = array( array( 'icon' => 'windows', 'title' => __( 'Work in windows', 'desktop-mode' ), 'desc' => __( 'Posts, media and settings side by side.', 'desktop-mode' ), ), array( 'icon' => 'apps', 'title' => __( 'Apps for everyday tasks', 'desktop-mode' ), 'desc' => __( 'Fast lists with bulk actions and previews.', 'desktop-mode' ), ), array( 'icon' => 'dock', 'title' => __( 'A dock for your screens', 'desktop-mode' ), 'desc' => __( 'Pin what you use most, one click away.', 'desktop-mode' ), ), array( 'icon' => 'command', /* translators: %s: the keyboard shortcut that opens search, e.g. ⌘K. */ 'title' => __( 'Search with %s', 'desktop-mode' ), 'desc' => __( 'Jump to any screen or run a command.', 'desktop-mode' ), 'kbd' => true, ), ); ?>