[menu label, screen id]. */ /** The Dashbread mark (assets/img/menu-icon.svg), fill-only so wp-admin can tint it for the color scheme. */ public const MENU_ICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIj48cGF0aCBmaWxsPSIjYTdhYWFkIiBkPSJNMi4yMCwxNi4yMEwyLjIwLDcuNDBMMi4yMSw3LjExTDIuMjQsNi44M0wyLjI4LDYuNTRMMi4zNSw2LjI2TDIuNDMsNS45OUwyLjUzLDUuNzJMMi42NSw1LjQ1TDIuNzksNS4yMEwyLjk0LDQuOTZMMy4xMSw0LjcyTDMuMjksNC41MEwzLjQ5LDQuMjlMMy43MCw0LjA5TDMuOTIsMy45MUw0LjE2LDMuNzRMNC40MCwzLjU5TDQuNjUsMy40NUw0LjkyLDMuMzNMNS4xOSwzLjIzTDUuNDYsMy4xNUw1Ljc0LDMuMDhMNi4wMywzLjA0TDYuMzEsMy4wMUw2LjYwLDMuMDBMNi45MywyLjk0TDcuMjcsMi44OEw3LjQwLDIuODZMNy40MCwxNy44MEwzLjgwLDE3LjgwTDMuNDksMTcuNzdMMy4xOSwxNy42OEwyLjkxLDE3LjUzTDIuNjcsMTcuMzNMMi40NywxNy4wOUwyLjMyLDE2LjgxTDIuMjMsMTYuNTFMMi4yMCwxNi4yMFoiLz48cGF0aCBmaWxsPSIjYTdhYWFkIiBkPSJNOC40MCwyLjcyTDguNjAsMi43MEw4LjkzLDIuNjdMOS4yNywyLjY0TDkuNjAsMi42M0w5LjkzLDIuNjFMMTAuMjcsMi42MEwxMC42MCwyLjYwTDEwLjkzLDIuNjBMMTEuMjcsMi42MUwxMS42MCwyLjYzTDExLjkzLDIuNjRMMTIuMjcsMi42N0wxMi42MCwyLjcwTDEyLjgwLDIuNzJMMTIuODAsMTcuODBMOC40MCwxNy44MFoiLz48cGF0aCBmaWxsPSIjYTdhYWFkIiBkPSJNMTMuODAsMi44NkwxMy45MywyLjg4TDE0LjI3LDIuOTRMMTQuNjAsMy4wMEwxNC44OSwzLjAxTDE1LjE3LDMuMDRMMTUuNDYsMy4wOEwxNS43NCwzLjE1TDE2LjAxLDMuMjNMMTYuMjgsMy4zM0wxNi41NSwzLjQ1TDE2LjgwLDMuNTlMMTcuMDQsMy43NEwxNy4yOCwzLjkxTDE3LjUwLDQuMDlMMTcuNzEsNC4yOUwxNy45MSw0LjUwTDE4LjA5LDQuNzJMMTguMjYsNC45NkwxOC40MSw1LjIwTDE4LjU1LDUuNDVMMTguNjcsNS43MkwxOC43Nyw1Ljk5TDE4Ljg1LDYuMjZMMTguOTIsNi41NEwxOC45Niw2LjgzTDE4Ljk5LDcuMTFMMTkuMDAsNy40MEwxOS4wMCwxNi4yMEwxOS4wMCwxNi4yMEwxOC45NywxNi41MUwxOC44OCwxNi44MUwxOC43MywxNy4wOUwxOC41MywxNy4zM0wxOC4yOSwxNy41M0wxOC4wMSwxNy42OEwxNy43MSwxNy43N0wxNy40MCwxNy44MEwxMy44MCwxNy44MFoiLz48L3N2Zz4='; public function register(): void { add_action( 'admin_menu', array( $this, 'menu' ) ); add_action( 'admin_init', array( $this, 'settings' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); add_filter( 'plugin_action_links_' . plugin_basename( DASHBREAD_FILE ), array( $this, 'links' ) ); } /** * Core screens plus any an add-on adds through `dashbread_admin_screens` * (slug suffix => [menu label, screen id]; the add-on's bundle registers the React screen). * * @return array */ public static function screens(): array { $screens = array( '' => array( __( 'Dashboard', 'dashbread' ), 'dashboard' ), '-settings' => array( __( 'Settings', 'dashbread' ), 'settings' ), '-hosted' => array( __( 'Hosted pages', 'dashbread' ), 'hosted' ), '-notifications' => array( __( 'Notifications', 'dashbread' ), 'notifications' ), '-extend' => array( __( 'Pro & Add-ons', 'dashbread' ), 'extend' ), '-help' => array( __( 'Help', 'dashbread' ), 'help' ), ); return (array) apply_filters( 'dashbread_admin_screens', $screens ); } public function menu(): void { // Menu titles accept markup: a "Pro" pill when the Dashbread Pro plugin is active (same as OrgFuse). $brand = Brand::name(); $menu_title = esc_html( $brand ) . ( Features::pro_active() && Brand::pro_pill() ? ' ' . esc_html__( 'Pro', 'dashbread' ) . '' : '' ); add_menu_page( $brand, $menu_title, 'manage_options', 'dashbread', array( $this, 'page' ), Brand::menu_icon(), 58 ); foreach ( self::screens() as $suffix => [ $label ] ) { add_submenu_page( 'dashbread', $brand . ' › ' . $label, $label, 'manage_options', 'dashbread' . $suffix, array( $this, 'page' ) ); } /** Lets Pro / add-ons register more screens under the Dashbread menu (after the Core ones). */ do_action( 'dashbread_admin_menu' ); } public function links( array $links ): array { array_unshift( $links, '' . esc_html__( 'Settings', 'dashbread' ) . '', '' . esc_html__( 'Open app', 'dashbread' ) . '' ); return $links; } public function settings(): void { register_setting( 'dashbread', Settings::OPTION, array( 'type' => 'array', 'sanitize_callback' => array( $this, 'sanitize' ) ) ); } /** The one place settings are validated: used by options.php and by the admin REST route. @param mixed $in */ public function sanitize( $in ): array { $in = is_array( $in ) ? $in : array(); $old = Settings::all(); $out = array( 'route' => sanitize_title( (string) ( $in['route'] ?? 'app' ) ) ?: 'app', 'site_name' => sanitize_text_field( (string) ( $in['site_name'] ?? '' ) ), 'logo_id' => (int) ( $in['logo_id'] ?? 0 ), 'primary' => sanitize_hex_color( (string) ( $in['primary'] ?? '#4f46e5' ) ) ?: '#4f46e5', 'mode' => in_array( $in['mode'] ?? 'light', array( 'light', 'dark', 'auto' ), true ) ? $in['mode'] : 'light', 'preset' => in_array( $in['preset'] ?? 'console', array( 'console', 'comfort' ), true ) ? $in['preset'] : 'console', 'redirect_login' => empty( $in['redirect_login'] ) ? 0 : 1, 'land_in_app' => empty( $in['land_in_app'] ) ? 0 : 1, 'hide_admin_bar' => empty( $in['hide_admin_bar'] ) ? 0 : 1, 'block_admin' => empty( $in['block_admin'] ) ? 0 : 1, 'legacy_login' => empty( $in['legacy_login'] ) ? 0 : 1, 'footer_text' => wp_kses_post( (string) ( $in['footer_text'] ?? '' ) ), 'powered_by' => empty( $in['powered_by'] ) ? 0 : 1, 'menus' => $old['menus'], ); if ( $out['route'] !== $old['route'] || $out['legacy_login'] !== $old['legacy_login'] ) { update_option( 'dashbread_flush_needed', 1 ); } return $out; } public static function is_screen( string $hook ): bool { // Submenu hooks derive from the sanitized menu title, which carries the "Pro" pill when Pro is // active (dashbread-pro_page_dashbread-settings), so match on the page slug part only. return 'toplevel_page_dashbread' === $hook || str_contains( $hook, '_page_dashbread' ); } public function enqueue( string $hook ): void { // The pill sits in the global admin menu, so its style loads on every admin screen. if ( Features::pro_active() ) { wp_register_style( 'dashbread-admin-menu', false, array(), DASHBREAD_VERSION ); wp_enqueue_style( 'dashbread-admin-menu' ); wp_add_inline_style( 'dashbread-admin-menu', '#adminmenu #toplevel_page_dashbread .dashbread-pro-pill{display:inline-block;margin-left:6px;padding:0 6px;font-size:9px;line-height:15px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;color:#fff;background:#4f46e5;border-radius:9px;vertical-align:middle;position:relative;top:-1px}#adminmenu #toplevel_page_dashbread.wp-has-current-submenu .dashbread-pro-pill,#adminmenu #toplevel_page_dashbread:hover .dashbread-pro-pill{background:#fff;color:#4f46e5}' ); } if ( ! self::is_screen( $hook ) ) { return; } wp_enqueue_media(); wp_enqueue_style( 'dashbread-tokens', DASHBREAD_URL . 'assets/css/tokens.css', array(), DASHBREAD_VERSION ); wp_enqueue_style( 'dashbread-admin', DASHBREAD_URL . 'assets/css/admin.css', array( 'dashbread-tokens' ), DASHBREAD_VERSION ); wp_enqueue_script( 'dashbread-admin', DASHBREAD_URL . 'assets/build/admin-app.js', array( 'react', 'react-dom', 'wp-api-fetch' ), DASHBREAD_VERSION, true ); \Dashbread\Core\I18n::script( 'dashbread-admin' ); // Dismissing one of Dashbread's notices (data-dashbread-dismiss="key") is remembered per user. wp_add_inline_script( 'dashbread-admin', "document.addEventListener('click',function(e){var b=e.target.closest&&e.target.closest('.notice[data-dashbread-dismiss] .notice-dismiss');if(!b)return;window.wp.apiFetch({path:'/" . \Dashbread\Rest\Routes::NS . "/admin/dismiss',method:'POST',data:{key:b.closest('.notice').getAttribute('data-dashbread-dismiss')}}).catch(function(){});});" ); wp_localize_script( 'dashbread-admin', 'DashbreadAdmin', array( 'rest' => esc_url_raw( rest_url( \Dashbread\Rest\Routes::NS . '/' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'adminUrl' => admin_url( 'admin.php' ), 'version' => DASHBREAD_VERSION, 'pro' => Features::pro_active(), 'icons' => \Dashbread\Shell\Shell::js_icons() + $this->admin_icons(), // The full set the icon picker offers; ui.jsx fetches it the first time an icon isn't preloaded. 'iconsJson' => DASHBREAD_URL . 'assets/icons/lucide.json', 'screens' => array_map( static fn( $suffix, $s ) => array( 'id' => $s[1], 'label' => $s[0], 'slug' => 'dashbread' . $suffix ), array_keys( self::screens() ), self::screens() ), 'site' => get_bloginfo( 'name' ), 'email' => wp_get_current_user()->user_email, 'year' => gmdate( 'Y' ), 'brand' => array( 'name' => Brand::name(), 'logo' => Brand::logo(), 'tagline' => Brand::tagline(), 'custom' => Brand::is_custom() ), ) ); } /** Lucide icons the admin screens' own chrome uses, preloaded; any other icon loads from lucide.json on demand. */ private function admin_icons(): array { return \Dashbread\Shell\Icons::inner_map( array( 'layout-dashboard', 'settings', 'files', 'bell', 'puzzle', 'circle-help', 'users', 'user-plus', 'log-in', 'mail-open', 'check', 'x', 'external-link', 'moon', 'sun', 'search', 'plus', 'trash-2', 'lock', 'sparkles', 'triangle-alert', 'circle-check', 'circle-x', 'building-2', 'message-square', 'send', 'layout-grid', 'menu', 'image', 'rocket', 'book-open', 'life-buoy', 'trash', 'refresh-cw', 'file-text', 'eye', 'pencil', 'arrow-left-right', 'circle', 'file', 'chart-line', 'chart-column', 'chart-pie', 'flag', 'list-todo', 'chevron-down', 'shield-check', 'user-check', 'messages-square', 'key-round', 'smartphone', 'link', 'unlink', 'copy', 'gift', 'calendar', 'mouse-pointer-click', 'copy-plus' ) ); } /** The SPA mount. Screen comes from the page slug. */ public function page(): void { $page = sanitize_key( (string) ( $_GET['page'] ?? 'dashbread' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only: which Dashbread screen to mount; WordPress has already checked manage_options for this page. $suffix = substr( $page, strlen( 'dashbread' ) ); $screen = self::screens()[ $suffix ][1] ?? 'dashboard'; echo '
' . esc_html__( 'Loading Dashbread…', 'dashbread' ) . '
'; } }