'Today', 'storesuit-orders' => 'Sales', 'storesuit-carts' => 'Live carts', 'storesuit-stock-alerts' => 'Waiting customers', 'storesuit-mail-log' => 'Email history', 'storesuit-redirects' => 'Redirects and 404s', 'storesuit-operations' => 'Order desk', 'storesuit-modules' => 'All features', 'storesuit-audit' => 'Activity log', 'storesuit-products' => 'Products', 'storesuit-customers' => 'Customers', 'storesuit-marketing' => 'Marketing', 'storesuit-reports' => 'Reports', 'storesuit-settings' => 'Settings & Help', 'storesuit-pro' => 'StoreRuler Pro', 'storesuit-order-tracking-settings' => 'Order tracking', ); private const LABELS = array(); // The engines' menu names in StoreRuler words: ProScreens::labels(). public static function label( string $text ) : string { $labels = self::LABELS; return I18n::t( $labels[$text] ?? $text ); } public static function hooks() : void { add_filter( 'admin_body_class', static function ( $classes ) { $page = self::query()['page'] ?? ''; $engine = ''; return $classes . (( self::context() ? ' ss-workspace' : '' )) . $engine; } ); add_action( 'admin_enqueue_scripts', array(__CLASS__, 'assets'), 9999 ); add_action( 'in_admin_header', array(__CLASS__, 'shell') ); add_filter( 'admin_title', array(__CLASS__, 'admin_title'), 10, 2 ); } /** * The browser tab names the screen as StoreRuler's header does. Bundled engines register their pages before their own * translations load, so the tab read "Store activity" or "Safe Catalog Bulk Editor" in a Turkish store. * WordPress escapes the result when it prints the tab title. */ public static function admin_title( $admin_title, $title ) { $page = (string) (self::query()['page'] ?? ''); $context = self::context(); // A screen without its own header title (a Pro engine screen) may name itself: storesuit_screen_title, already translated. $shown = ( is_string( $context['title'] ?? null ) && $context['title'] !== '' ? $context['title'] : (string) apply_filters( 'storesuit_screen_title', '', $page ) ); $shown = trim( wp_strip_all_tags( (string) $shown ) ); if ( $shown === '' || !is_string( $admin_title ) ) { return $admin_title; } $title = ( is_string( $title ) ? $title : '' ); if ( $title === '' ) { return $shown . $admin_title; } return ( str_starts_with( $admin_title, $title ) ? $shown . substr( $admin_title, strlen( $title ) ) : $admin_title ); } private static function query() : array { $query = array(); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Which admin page is shown, for the menu and the page title; nothing is changed. foreach ( array('page', 'view', 'tab') as $key ) { if ( isset( $_GET[$key] ) && is_string( $_GET[$key] ) ) { $query[$key] = sanitize_key( wp_unslash( $_GET[$key] ) ); } } // phpcs:enable WordPress.Security.NonceVerification.Recommended return $query; } /** Whether the logo's page (page=storesuit) opens Home rather than the day's work list: the screen's own rule (admin.js), for people who manage the store and see its money, and no view=today. */ public static function home_page() : bool { return (self::query()['view'] ?? '') !== 'today' && current_user_can( 'manage_woocommerce' ) && current_user_can( 'storesuit_finance' ); } private static function match( string $url ) : int { parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $target ); $current = self::query(); if ( empty( $target['page'] ) || ($current['page'] ?? '') !== $target['page'] ) { return 0; } foreach ( array('view', 'tab') as $key ) { if ( isset( $target[$key] ) && ($current[$key] ?? '') !== $target[$key] ) { return 0; } } return count( array_intersect_key( $target, array_flip( array('page', 'view', 'tab') ) ) ); } public static function context() : array { $page = self::query()['page'] ?? ''; // merchuno-support-v1: keep the Support page inside the existing workspace shell. if ( $page === 'storesuit-support' && class_exists( '\\MerchunoSupport\\Client' ) ) { return array( 'title' => \MerchunoSupport\Client::say( 'Support' ), 'group' => 'StoreRuler', 'key' => 'help', ); } if ( $page === 'storesuit-guide' ) { return array( 'title' => I18n::t( 'Step-by-step guide' ), 'group' => 'StoreRuler', 'key' => 'suite', ); } $pages = self::PAGES; if ( isset( $pages[$page] ) ) { $key = ( $page === 'storesuit' || $page === 'storesuit-modules' ? 'suite' : Navigation::area( admin_url( 'admin.php?page=' . $page ) ) ); $title = ( $page === 'storesuit' && (self::query()['view'] ?? '') !== 'today' ? 'Home' : $pages[$page] ); return array( 'title' => I18n::t( $title ), 'group' => 'StoreRuler', 'key' => $key, ); } $found = array(); $score = 0; foreach ( Admin::navigation() as $group ) { foreach ( $group['children'] as $child ) { $match = self::match( $child['url'] ); if ( $match > $score ) { $score = $match; $found = array( 'title' => self::label( $child['name'] ), 'group' => $group['name'], 'key' => $group['key'], ); } } } return $found; } public static function assets() : void { if ( !self::context() ) { return; } wp_enqueue_style( 'dashicons' ); wp_enqueue_style( 'storesuit-design', plugins_url( 'assets/design.css', STORESUIT_FILE ), array(), (string) filemtime( STORESUIT_DIR . 'assets/design.css' ) ); wp_enqueue_style( 'storesuit-mega', plugins_url( 'assets/mega.css', STORESUIT_FILE ), array('storesuit-design'), (string) filemtime( STORESUIT_DIR . 'assets/mega.css' ) ); wp_enqueue_style( 'storesuit-engine-ledger', plugins_url( 'assets/engine-ledger.css', STORESUIT_FILE ), array('storesuit-mega'), (string) filemtime( STORESUIT_DIR . 'assets/engine-ledger.css' ) ); I18n::enqueue_script(); wp_enqueue_script( 'storesuit-shell', plugins_url( 'assets/shell.js', STORESUIT_FILE ), array('storesuit-i18n'), (string) filemtime( STORESUIT_DIR . 'assets/shell.js' ), true ); wp_localize_script( 'storesuit-shell', 'StoreSuitShellConfig', array( 'url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'storesuit' ), ) ); // StoreRuler Premium (0.62): one visual layer over every StoreRuler and tool screen, loaded last. wp_enqueue_style( 'storesuit-premium', plugins_url( 'assets/premium.css', STORESUIT_FILE ), array('storesuit-engine-ledger'), (string) filemtime( STORESUIT_DIR . 'assets/premium.css' ) ); wp_enqueue_style( 'storesuit-layout-fixes', plugins_url( 'assets/layout-fixes.css', STORESUIT_FILE ), array('storesuit-premium'), (string) filemtime( STORESUIT_DIR . 'assets/layout-fixes.css' ) ); wp_enqueue_script( 'storesuit-layout-fixes', plugins_url( 'assets/layout-fixes.js', STORESUIT_FILE ), array('storesuit-shell'), (string) filemtime( STORESUIT_DIR . 'assets/layout-fixes.js' ), true ); self::guide( self::query()['page'] ?? '' ); self::no_command_palette( self::query()['page'] ?? '' ); } /** * WordPress's command palette (Ctrl+K) and WooCommerce's commands for it stay off StoreRuler's own screens, which have * their menu and search in the shell (owner, 20 Oct 2026). With them go the two clients WordPress and WooCommerce put on * every admin screen for the editor's data store (wp-abilities, wc-entities), which nothing on these screens reads. Together * they brought React and the block editor's packages, about 3 MB of script the browser ran again on each tab. When another * script of the screen uses that data store or the block editor (the Design studio's editor), the two clients stay; * other plugins' screens keep everything. */ private static function no_command_palette( string $page ) : void { if ( strpos( $page, 'storesuit' ) !== 0 ) { return; } foreach ( array( 'wp-core-commands', 'wp-commands', 'wc-admin-command-palette', 'wc-admin-command-palette-analytics' ) as $handle ) { wp_dequeue_script( $handle ); } wp_dequeue_style( 'wp-commands' ); $scripts = wp_scripts(); $clients = array('wp-abilities', 'wc-entities'); $uses = static function ( string $handle, array &$seen ) use(&$uses, $scripts) : bool { if ( in_array( $handle, array('wp-core-data', 'wp-block-editor', 'wp-blocks'), true ) ) { return true; } if ( isset( $seen[$handle] ) || !isset( $scripts->registered[$handle] ) ) { return false; } $seen[$handle] = true; foreach ( (array) $scripts->registered[$handle]->deps as $dep ) { if ( $uses( (string) $dep, $seen ) ) { return true; } } return false; }; foreach ( $scripts->queue as $handle ) { $seen = array(); if ( !in_array( $handle, $clients, true ) && $uses( (string) $handle, $seen ) ) { return; } } foreach ( $clients as $handle ) { wp_dequeue_script( $handle ); } } /** The kind of module guide each free screen gets (page-onboarding.js picks its steps by it); ProScreens::guide() adds the Pro ones. */ private const GUIDES = array( 'storesuit' => 'home', 'storesuit-orders' => 'orders', 'storesuit-operations' => 'orders', 'storesuit-products' => 'products', 'storesuit-customers' => 'customers', 'storesuit-marketing' => 'marketing', 'storesuit-carts' => 'marketing', 'storesuit-stock-alerts' => 'marketing', 'storesuit-redirects' => 'marketing', 'storesuit-reports' => 'reports', 'storesuit-settings' => 'settings', 'storesuit-audit' => 'settings', 'storesuit-mail-log' => 'settings', 'storesuit-order-tracking-settings' => 'settings', 'storesuit-guide' => 'help', 'storesuit-modules' => 'help', ); /** * The module guide (1.2.8): on a person's first visit to a StoreRuler module, page-onboarding.js points at the screen's * own controls one at a time; afterwards "Open guide" brings it back. What it keeps (the modules seen, and whether guides * open by themselves) stays in that browser's localStorage under the site's path and the user's number, so the page * gives it those and the module: no name, e-mail, nonce or other secret, and it makes no request. StoreRuler Pro's page * (an offer, not a module), a screen of another plugin and a screen without a module get none. */ private static function guide( string $page ) : void { $family = self::GUIDES[$page] ?? ''; if ( $family === '' ) { return; } wp_enqueue_style( 'storesuit-page-onboarding', plugins_url( 'assets/page-onboarding.css', STORESUIT_FILE ), array('storesuit-premium'), STORESUIT_VERSION . '.' . filemtime( STORESUIT_DIR . 'assets/page-onboarding.css' ) ); wp_enqueue_script( 'storesuit-page-onboarding', plugins_url( 'assets/page-onboarding.js', STORESUIT_FILE ), array('storesuit-i18n'), STORESUIT_VERSION . '.' . filemtime( STORESUIT_DIR . 'assets/page-onboarding.js' ), true ); // A module is a page, whatever report, tab or filter it shows; the purchasing pages (goods in, the purchase desk) are one. $path = (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ); wp_localize_script( 'storesuit-page-onboarding', 'StoreSuitOnboardingConfig', array( 'userId' => get_current_user_id(), 'sitePath' => ( $path !== '' ? $path : '/' ), 'module' => ( $family === 'purchasing' ? 'purchasing' : $page ), 'family' => $family, ) ); } private static function icon( string $name ) : string { return ''; } private static function item( string $name, string $url, string $icon = '', bool $active = false ) : void { echo '' . (( $icon ? wp_kses_post( self::icon( $icon ) ) : '' )) . '' . esc_html( $name ) . ''; } private static function daily_active( string $route ) : bool { parse_str( (string) wp_parse_url( admin_url( 'admin.php?page=' . $route ), PHP_URL_QUERY ), $target ); $current = array(); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Which menu item is the page being shown; nothing is changed. foreach ( array( 'page', 'flow', 'new', 'queue' ) as $key ) { if ( isset( $_GET[$key] ) && is_string( $_GET[$key] ) ) { $current[$key] = sanitize_key( wp_unslash( $_GET[$key] ) ); } } // phpcs:enable WordPress.Security.NonceVerification.Recommended return $target == $current; } /** * Words the top-bar search also finds a menu entry by (shell.js reads them from data-keywords), by route: English source * text, comma-separated, translated like any other text. StoreRuler Pro adds its own (ProScreens::keywords()). */ private const KEYWORDS = array(); /** A menu entry's search words: the English ones and, in another language, the translated ones too ('' for none). */ private static function keywords( string $url ) : string { $table = self::KEYWORDS; $key = Guide::key( $url ); $words = (string) ($table[$key] ?? $table[explode( ':', $key, 2 )[0]] ?? ''); if ( $words === '' ) { return ''; } $local = I18n::t( $words ); return ( $local === $words ? $words : $words . ', ' . $local ); } private static function mega_link( string $title, string $url, string $description = '', bool $active = false ) : void { $keywords = self::keywords( $url ); // The description shows on one line; the whole sentence stays available on hover. echo '' . esc_html( $title ) . '' . (( $description ? '' . esc_html( $description ) . '' : '' )) . ''; } private static function mega_daily( array $row ) : void { $url = admin_url( 'admin.php?page=' . $row[1] ); $task = Guide::describe( $url ); self::mega_link( $row[0], $url, $row[3] ?? $task[1] ?? '', self::daily_active( $row[1] ) ); } /** An area's two tabs when it has tools beyond its daily work: that work, and the other tools with their count (0.62). */ private static function mega_tabs( array $group ) : void { if ( !$group['children'] ) { return; } $more = ( $group['key'] === 'sales' ? I18n::t( 'More tools' ) : (string) $group['management_label'] ); echo '
' . esc_html( $group['description'] ) . '