From f2c86bed784292f6683cdbf96bca99b2dd9dc8f5 Mon Sep 17 00:00:00 2001 From: fahimreza-dev Date: Fri, 9 Oct 2026 16:01:22 +0600 Subject: [PATCH 1/4] =?UTF-8?q?feat(admin):=20move=20the=20app=20from=20a?= =?UTF-8?q?=20top-level=20menu=20to=20settings=20=E2=86=92=20smtp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registers the page with add_options_page() (screen settings_page_mailyard) and drops the top-level menu, its five submenu entries, the menu icon and the submenu click interceptor. Old admin.php?page=mailyard URLs redirect with their query args; every internal link goes through Settings::url(). The send-failure notice now shows on the Dashboard and Plugins screens only, with one dismiss path. Refs #20 Co-Authored-By: Claude Opus 5.5 --- includes/class-alerts.php | 7 +- includes/class-conflicts.php | 2 +- includes/class-failure-notice.php | 16 +++-- includes/class-oauth.php | 2 +- includes/class-plugin.php | 2 +- includes/class-settings.php | 112 +++++++++--------------------- readme.txt | 7 +- 7 files changed, 53 insertions(+), 95 deletions(-) diff --git a/includes/class-alerts.php b/includes/class-alerts.php index 2e5e680..af8a982 100644 --- a/includes/class-alerts.php +++ b/includes/class-alerts.php @@ -136,7 +136,7 @@ public function test( string $channel, string $target = '' ) { */ public static function webhook_payload( string $url, string $event, string $text, array $extra = array() ): array { $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) ); - $logs = admin_url( 'admin.php?page=mailyard#/logs' ); + $logs = Settings::url( 'logs' ); if ( preg_match( '/(^|\.)slack\.com$/', $host ) ) { return array( 'text' => $text . "\n" . $logs ); @@ -183,7 +183,7 @@ private function claim( string $kind ): bool { private function notify( string $subject, array $lines, string $event, array $extra ): void { if ( ! empty( Options::settings()['alert_email'] ) ) { - $this->send_unrouted( $this->recipient(), $subject, implode( "\n\n", $lines ) . "\n\n" . __( 'The email log:', 'mailyard' ) . ' ' . admin_url( 'admin.php?page=mailyard#/logs' ) . $this->footer() ); + $this->send_unrouted( $this->recipient(), $subject, implode( "\n\n", $lines ) . "\n\n" . __( 'The email log:', 'mailyard' ) . ' ' . Settings::url( 'logs' ) . $this->footer() ); } $this->post_webhook( $event, $subject . ' — ' . implode( ' ', array_slice( $lines, 1 ) ), $extra ); } @@ -232,6 +232,7 @@ private function site_name(): string { } private function footer(): string { - return "\n\n— " . __( 'Sent by Mailyard. Change alerts in Mailyard → Settings → Alerts.', 'mailyard' ); + /* translators: %s: URL of Mailyard's alert settings. */ + return "\n\n— " . sprintf( __( 'Sent by Mailyard. Change alerts: %s', 'mailyard' ), Settings::url( 'settings/alerts' ) ); } } diff --git a/includes/class-conflicts.php b/includes/class-conflicts.php index 52ad5f6..5f5e407 100644 --- a/includes/class-conflicts.php +++ b/includes/class-conflicts.php @@ -49,7 +49,7 @@ public function maybe_show_notice(): void { } $screen = get_current_screen(); - if ( $screen && ! in_array( $screen->id, array( 'toplevel_page_mailyard', 'plugins' ), true ) ) { + if ( $screen && ! in_array( $screen->id, array( Settings::SCREEN, 'plugins' ), true ) ) { return; } diff --git a/includes/class-failure-notice.php b/includes/class-failure-notice.php index 2420661..9528705 100644 --- a/includes/class-failure-notice.php +++ b/includes/class-failure-notice.php @@ -4,8 +4,8 @@ defined( 'ABSPATH' ) || exit; /** - * Surfaces email send failures as a dismissible admin notice with a link to the - * log. Records the latest failure (fed by the `mailyard_send_failed` action) and + * Surfaces email send failures as a dismissible admin notice (Dashboard and + * Plugins screens) with a link to the log. Records the latest failure (fed by the `mailyard_send_failed` action) and * auto-clears it when a later send succeeds or the connection config changes, so * a fixed setup dismisses the warning on its own — it isn't nagware. */ @@ -82,9 +82,10 @@ public function render(): void { return; } - // The log is already on the Mailyard admin page — don't double up there. + // Only where an admin lands — the Dashboard and Plugins screens — not + // on every admin page. Mailyard's own page shows it in its overview. $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; - if ( $screen && false !== strpos( (string) $screen->id, 'page_mailyard' ) ) { + if ( ! $screen || ! in_array( $screen->id, array( 'dashboard', 'plugins' ), true ) ) { return; } @@ -98,7 +99,7 @@ public function render(): void { $title = isset( $failure['title'] ) ? $failure['title'] : __( 'Email could not be sent', 'mailyard' ); $guidance = isset( $failure['guidance'] ) ? $failure['guidance'] : ''; - $log_url = admin_url( 'admin.php?page=mailyard#/logs?status=failed' ); + $log_url = Settings::url( 'logs?status=failed' ); $dismiss_url = wp_nonce_url( add_query_arg( 'mailyard_dismiss_failure', '1' ), 'mailyard_dismiss_failure' @@ -109,11 +110,12 @@ public function render(): void { ? sprintf( _n( 'Mailyard: %d email failed to send', 'Mailyard: %d emails failed to send recently', $count, 'mailyard' ), $count ) : __( 'Mailyard: an email failed to send', 'mailyard' ); + // One dismiss path: the link (per user, until the next failure). No + // `is-dismissible` × — it only hides the notice until the next page load. printf( - '

%s — %s %s

%s %s

', + '

%s — %s

%s %s

', esc_html( $heading . ( $title ? ': ' . $title : '' ) ), esc_html( $guidance ), - '', esc_url( $log_url ), esc_html__( 'View email log', 'mailyard' ), esc_url( $dismiss_url ), diff --git a/includes/class-oauth.php b/includes/class-oauth.php index b3779ff..3ef927a 100644 --- a/includes/class-oauth.php +++ b/includes/class-oauth.php @@ -141,7 +141,7 @@ public static function return_url( $result ): string { $args = is_wp_error( $result ) ? array( 'mailyard_oauth' => 'error', 'message' => $result->get_error_message() ) : array( 'mailyard_oauth' => 'ok' ); - return admin_url( 'admin.php?page=mailyard&' . http_build_query( $args, '', '&', PHP_QUERY_RFC3986 ) . '#/connections' ); + return add_query_arg( rawurlencode_deep( $args ), admin_url( 'options-general.php?page=' . Settings::PAGE ) ) . '#/connections'; } /** diff --git a/includes/class-plugin.php b/includes/class-plugin.php index 149735c..906e90f 100644 --- a/includes/class-plugin.php +++ b/includes/class-plugin.php @@ -130,7 +130,7 @@ public function run_cleanup(): void { } public function plugin_action_links( array $links ): array { - $url = admin_url( 'admin.php?page=mailyard' ); + $url = Settings::url(); $label = __( 'Settings', 'mailyard' ); array_unshift( $links, '' . $label . '' ); return $links; diff --git a/includes/class-settings.php b/includes/class-settings.php index 49fa7da..113d582 100644 --- a/includes/class-settings.php +++ b/includes/class-settings.php @@ -6,6 +6,11 @@ // Admin page and asset loading for the Mailyard React UI. class Settings { + const PAGE = 'mailyard'; + + // The page's screen id / hook suffix under Settings. + const SCREEN = 'settings_page_mailyard'; + private static $instance = null; public static function instance(): self { @@ -22,72 +27,58 @@ public function init() { add_action( 'admin_init', array( $this, 'redirect_legacy_url' ) ); } + /** + * The admin page URL, optionally at an app route ('logs', 'connections', …). + * Every link into the app goes through here. + */ + public static function url( string $route = '' ): string { + $url = admin_url( 'options-general.php?page=' . self::PAGE ); + return '' === $route ? $url : $url . '#/' . ltrim( $route, '#/' ); + } + /** * PlugPress design system token scope on , so portaled overlays * (dialogs, dropdowns, toasts) inherit the --pp-* custom properties. */ public function body_class( $classes ) { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; - if ( $screen && 'toplevel_page_mailyard' === $screen->id ) { + if ( $screen && self::SCREEN === $screen->id ) { $classes .= ' pp-scope'; } return $classes; } + // Settings → SMTP: one page, the app's own top bar handles the sections. public function add_menu() { - // '58.14' is Mailyard's reserved slot in the PlugPress admin-menu band - // (plugpress-ui php/menu-positions.php) — a decimal STRING so it can - // never clobber a third-party menu registered at the same integer. - add_menu_page( - __( 'Mailyard', 'mailyard' ), - __( 'Mailyard', 'mailyard' ), + add_options_page( + __( 'Mailyard SMTP', 'mailyard' ), + __( 'SMTP', 'mailyard' ), 'manage_options', - 'mailyard', - array( $this, 'render' ), - $this->menu_icon(), - '58.14' - ); - - // One native submenu entry per page, mirroring the app's sidebar (NAV in - // src/App.jsx). Keys are the SPA hash route each entry lands on. - $submenus = array( - 'dashboard' => __( 'Dashboard', 'mailyard' ), - 'connections' => __( 'Connections', 'mailyard' ), - 'logs' => __( 'Email log', 'mailyard' ), - 'deliverability' => __( 'Deliverability', 'mailyard' ), - 'settings' => __( 'Settings', 'mailyard' ), + self::PAGE, + array( $this, 'render' ) ); - - $first = true; - foreach ( $submenus as $key => $label ) { - add_submenu_page( - 'mailyard', - $label, - $label, - 'manage_options', - $first ? 'mailyard' : 'mailyard#/' . $key, - array( $this, 'render' ) - ); - $first = false; - } } /** - * 302 the pre-1.2 Settings-page URL to the top-level menu. Browsers carry - * the URL fragment across the redirect, so old deep bookmarks - * (options-general.php?page=mailyard#/logs) keep working. + * 302 the old top-level menu URL (admin.php?page=mailyard) to Settings → + * SMTP, keeping its query args (an OAuth return carries its outcome there). + * Browsers carry the URL fragment across the redirect, so old deep links + * (…page=mailyard#/logs, in alert emails already sent) keep working. */ public function redirect_legacy_url() { global $pagenow; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only redirect. - if ( 'options-general.php' === $pagenow && 'mailyard' === sanitize_key( wp_unslash( $_GET['page'] ?? '' ) ) ) { - wp_safe_redirect( admin_url( 'admin.php?page=mailyard' ) ); - exit; + if ( 'admin.php' !== $pagenow || self::PAGE !== sanitize_key( wp_unslash( $_GET['page'] ?? '' ) ) ) { + return; } + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only redirect. + $args = map_deep( wp_unslash( $_GET ), 'sanitize_text_field' ); + wp_safe_redirect( add_query_arg( rawurlencode_deep( $args ), admin_url( 'options-general.php' ) ) ); + exit; } public function enqueue( $hook ) { - if ( 'toplevel_page_mailyard' !== $hook ) { + if ( self::SCREEN !== $hook ) { return; } @@ -128,43 +119,6 @@ private function locked_fields(): object { } public function render() { - ?> -
- - ' - . '' - . ''; - - return 'data:image/svg+xml;base64,' . base64_encode( $svg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + echo '
'; } } diff --git a/readme.txt b/readme.txt index a1aad4f..8dcc55d 100644 --- a/readme.txt +++ b/readme.txt @@ -111,9 +111,9 @@ Terms: https://www.cloudflare.com/website-terms/ — Privacy: https://developers == Installation == 1. Search for "Mailyard" in Plugins → Add New (or upload the zip), then activate. -2. Open the Mailyard menu in your admin sidebar. +2. Open Settings → SMTP. 3. Pick a provider, enter your API key, set your sender address. -4. Hit Send test on the Dashboard to confirm it works. +4. Hit Send test on the Overview to confirm it works. = Adding a backup provider = @@ -203,7 +203,8 @@ Yes. Every feature you can see is yours — failover, routing, bounce tracking, * Add: Setup checks on the Dashboard — another plugin replacing wp_mail(), a From address on another domain, and Contact Form 7 / WPForms / Gravity Forms set to send "from" the visitor. * Update: Any logged email can be resent, not only failed ones. * Add: Send test names the provider that delivered it, and says so when a backup had to take over. -* Update: Simpler admin — one sidebar (Dashboard, Connections, Email log, Deliverability, Settings), mirrored in the WordPress menu. +* Update: Mailyard now lives under Settings → SMTP instead of its own top-level menu, with one slim top bar (Overview, Connections, Email log, Deliverability, Settings) and settings sections as tabs. Old links and bookmarks redirect. +* Update: The "email failed to send" notice shows on the Dashboard and Plugins screens only, with a single Dismiss link. * Update: Connections no longer have a Marketing/Transactional purpose; every enabled connection carries all mail. A connection that was set to Marketing only is switched off on update (its settings are kept). * Update: An email to several recipients goes out as one message, as WordPress sends it, instead of one copy per address. * Fix: Cc and Bcc addresses set in email headers were dropped by every provider. From 745042e89e917e4a765f0c708f0773beb9d9bf24 Mon Sep 17 00:00:00 2001 From: fahimreza-dev Date: Fri, 9 Oct 2026 16:13:28 +0600 Subject: [PATCH 2/4] feat(admin): top bar instead of sidebar, settings sections as tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app shell switches to the design system's topbar variant (brand · Overview · Connections · Email log · Deliverability · Settings · Help), so wp-admin's own sidebar is the only vertical nav. The submenu-highlight sync and the sidebar divider CSS go with the old menu. Settings drops its grouped left rail for one header with a tab row; each section opens with a one-line intro (SectionIntro) instead of repeating its name as a page title. Tabs scroll sideways on narrow screens. The base border-color rule now has zero specificity (:where), so library components keep their own border colors (the top bar's underlines) and authored border-* utilities render as written. Docs and readme paths follow the move; Dashboard is now Overview. Refs #20 Co-Authored-By: Claude Opus 5.5 --- docs/documentation.md | 26 ++++----- readme.txt | 8 +-- src/App.jsx | 48 +++++------------ src/components/Logo.jsx | 2 +- src/components/Nav.jsx | 56 ++++++++++++++++++++ src/components/Sidebar.jsx | 74 -------------------------- src/components/ui/index.js | 15 +++++- src/styles/globals.css | 21 +++----- src/views/AlertsSettings.jsx | 4 +- src/views/ConnectAI.jsx | 10 ++-- src/views/Dashboard.jsx | 2 +- src/views/NetworkSettings.jsx | 4 +- src/views/SecuritySettings.jsx | 4 +- src/views/Settings.jsx | 97 ++++++++++++---------------------- 14 files changed, 151 insertions(+), 220 deletions(-) create mode 100644 src/components/Nav.jsx delete mode 100644 src/components/Sidebar.jsx diff --git a/docs/documentation.md b/docs/documentation.md index 202dda5..73adf33 100644 --- a/docs/documentation.md +++ b/docs/documentation.md @@ -33,7 +33,7 @@ Amazon's Simple Email Service. Very low cost at volume ($0.10 per 1,000 emails). 1. In AWS Console → SES → Verified identities, verify your sending domain. 2. Create an IAM user with `ses:SendRawEmail` permission. 3. Generate an access key and secret for that user. -4. In Mailyard → Connections → Add → Amazon SES, paste the key, secret, and region. +4. In Settings → SMTP → Connections → Add → Amazon SES, paste the key, secret, and region. **Required fields:** Access Key ID, Secret Access Key, AWS Region (e.g. `us-east-1`). @@ -48,7 +48,7 @@ Premium transactional email. Known for the fastest delivery and best inbox place **Setup:** 1. In Postmark → Sender Signatures, add and verify your sending domain. 2. Create a Server and copy its Server API Token. -3. In Mailyard → Connections → Add → Postmark, paste the token. +3. In Settings → SMTP → Connections → Add → Postmark, paste the token. **Required fields:** Server API Token. @@ -65,7 +65,7 @@ Modern email API with a generous free tier (3,000 emails/month free). Simple RES **Setup:** 1. In Resend → Domains, add and verify your sending domain. 2. In Resend → API Keys, create a key. -3. In Mailyard → Connections → Add → Resend, paste the key. +3. In Settings → SMTP → Connections → Add → Resend, paste the key. **Required fields:** API Key. @@ -78,7 +78,7 @@ Free tier includes 300 emails per day with no monthly cap on contacts. **Setup:** 1. In Brevo → Senders & Domains, authenticate your domain (adds DKIM and SPF records). 2. In Brevo → SMTP & API → API Keys, create a key. -3. In Mailyard → Connections → Add → Brevo, paste the key. +3. In Settings → SMTP → Connections → Add → Brevo, paste the key. **Required fields:** API Key. @@ -90,7 +90,7 @@ Use any SMTP server: Gmail app passwords, Microsoft 365, Mailgun SMTP, your own **Setup:** 1. Gather your SMTP host, port, encryption type, and credentials from your provider. -2. In Mailyard → Connections → Add → Custom SMTP, fill in the fields. +2. In Settings → SMTP → Connections → Add → Custom SMTP, fill in the fields. **Required fields:** SMTP Host, Port (default 587). @@ -113,7 +113,7 @@ Uses WordPress's own `wp_mail()` without modification — whatever your hosting ### Priority order -Connections are attempted in priority order (1 = primary, 2 = first backup, etc.). Drag and drop in Mailyard → Connections to reorder them. +Connections are attempted in priority order (1 = primary, 2 = first backup, etc.). Drag and drop in Settings → SMTP → Connections to reorder them. The **primary** connection handles every email under normal conditions. If it returns an error, Mailyard immediately tries the next enabled connection — still inside the same `wp_mail()` call. @@ -144,7 +144,7 @@ The routing engine filters connections by purpose before selecting the chain. ## Email logging -When logging is enabled (Settings → Email logging), every outgoing email is recorded to a custom database table (`wp_mailyard_logs`). +When logging is enabled (Settings → SMTP → Settings → Delivery → Email logging), every outgoing email is recorded to a custom database table (`wp_mailyard_logs`). ### What is logged @@ -161,7 +161,7 @@ When logging is enabled (Settings → Email logging), every outgoing email is re ### Viewing logs -Go to Mailyard → Logs. You can: +Go to Settings → SMTP → Email log. You can: - Filter by status (All / Sent / Failed) - Search by recipient address or subject - Page through results (20 per page) @@ -170,13 +170,13 @@ Go to Mailyard → Logs. You can: Logs older than 30 days are deleted automatically via a daily WP-Cron job (`mailyard_daily_cleanup`). This keeps the database from growing unbounded. -To disable logging entirely, go to Settings → Email logging and toggle it off. Existing logs are kept until they age out or you uninstall the plugin. +To disable logging entirely, go to Settings → SMTP → Settings → Delivery and switch Email logging off. Existing logs are kept until they age out or you uninstall the plugin. --- ## Deliverability checker -Mailyard → Deliverability scans your sending domain's DNS records and scores your authentication setup from 0–100. +Settings → SMTP → Deliverability scans your sending domain's DNS records and scores your authentication setup from 0–100. ### What it checks @@ -249,7 +249,7 @@ Click **Send test** to send a real email through your live routing chain. Leave ## Connect AI (Abilities API / MCP) -Mailyard registers its delivery tools on the **WordPress Abilities API** (WordPress 7.0+), so an AI assistant can diagnose email problems for you. Control everything in **Settings → Connect AI**. +Mailyard registers its delivery tools on the **WordPress Abilities API** (WordPress 7.0+), so an AI assistant can diagnose email problems for you. Control everything in **Settings → SMTP → Settings → Connect AI**. ### The tools @@ -265,14 +265,14 @@ Every tool requires the `manage_options` capability. Credentials (`config`), and ### Permissions -**Settings → Connect AI** has a master switch ("AI access") and a switch per tool. Turning the master off unregisters every Mailyard ability immediately. The four read-only tools are on by default; `mailyard/send-test-email` is OFF by default because it delivers a real email — turn it on deliberately. +**Settings → SMTP → Settings → Connect AI** has a master switch ("AI access") and a switch per tool. Turning the master off unregisters every Mailyard ability immediately. The four read-only tools are on by default; `mailyard/send-test-email` is OFF by default because it delivers a real email — turn it on deliberately. ### Connecting a client Mailyard exposes abilities but ships **no MCP server** — install a bridge, e.g. the free [WordPress MCP Adapter](https://github.com/wordpress/mcp-adapter). Then: 1. Create an Application Password (**Users → Profile → Application Passwords**). -2. Copy the endpoint from Settings → Connect AI (the "How to connect" guide has copy buttons). +2. Copy the endpoint from Settings → SMTP → Settings → Connect AI (the "How to connect" guide has copy buttons). 3. Add it to your client, e.g. Claude Code: ```bash diff --git a/readme.txt b/readme.txt index 8dcc55d..d8fa66a 100644 --- a/readme.txt +++ b/readme.txt @@ -172,7 +172,7 @@ Yes. Every feature you can see is yours — failover, routing, bounce tracking, == Screenshots == 1. Setup — pick a provider, paste your key, set your sender address. -2. Dashboard — sending health, the last 14 days of volume, recent activity. +2. Overview — sending health, the last 14 days of volume, recent activity. 3. Connections — add providers and drag to choose the primary and its backup. 4. Deliverability — every sending domain graded A–F, with the exact DNS fix for anything that fails. 5. Email Logs — every send, with status and the error if it failed. @@ -199,12 +199,12 @@ Yes. Every feature you can see is yours — failover, routing, bounce tracking, * Add: Optional encryption of stored credentials, with a clear warning if the site's security keys change. * Add: Multisite shared settings — set email up once on the main site and let every site use it; the sender and delivery options are each optional to share. Logs always stay per site. * Add: Settings backup — export and import settings and connections as one file; empty the log on its own. -* Add: Dashboard panel with the week's most common failure reasons, in plain words. -* Add: Setup checks on the Dashboard — another plugin replacing wp_mail(), a From address on another domain, and Contact Form 7 / WPForms / Gravity Forms set to send "from" the visitor. +* Add: Overview panel with the week's most common failure reasons, in plain words. +* Add: Setup checks on the Overview — another plugin replacing wp_mail(), a From address on another domain, and Contact Form 7 / WPForms / Gravity Forms set to send "from" the visitor. * Update: Any logged email can be resent, not only failed ones. * Add: Send test names the provider that delivered it, and says so when a backup had to take over. * Update: Mailyard now lives under Settings → SMTP instead of its own top-level menu, with one slim top bar (Overview, Connections, Email log, Deliverability, Settings) and settings sections as tabs. Old links and bookmarks redirect. -* Update: The "email failed to send" notice shows on the Dashboard and Plugins screens only, with a single Dismiss link. +* Update: The "email failed to send" notice shows on the WordPress Dashboard and Plugins screens only, with a single Dismiss link. * Update: Connections no longer have a Marketing/Transactional purpose; every enabled connection carries all mail. A connection that was set to Marketing only is switched off on update (its settings are kept). * Update: An email to several recipients goes out as one message, as WordPress sends it, instead of one copy per address. * Fix: Cc and Bcc addresses set in email headers were dropped by every provider. diff --git a/src/App.jsx b/src/App.jsx index d7b934a..11dcadc 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,7 +1,6 @@ import { useState, useCallback, useEffect, Suspense, lazy } from 'react'; import { AppShell, Toaster } from '@plugpress/ui'; -import Sidebar from './components/Sidebar'; -import { GridIcon, RouteIcon, ShieldIcon, ListIcon, GearIcon } from './components/Icons'; +import Nav from './components/Nav'; import { DashboardSkeleton, ConnectionsSkeleton, @@ -15,24 +14,14 @@ const Deliverability = lazy( () => import( './views/Deliverability' ) ); const Logs = lazy( () => import( './views/Logs' ) ); const Settings = lazy( () => import( './views/Settings' ) ); -// The whole navigation: one list of pages, Settings pinned to the footer. -// Keys are the hash routes; nested routes (settings/connect-ai) belong to the -// first segment. +// The whole navigation: one row of pages in the top bar. Routes are the hash +// routes; nested routes (settings/connect-ai) belong to the first segment. const NAV = [ - { - id: 'main', - items: [ - { id: 'dashboard', label: 'Dashboard', icon: GridIcon, route: 'dashboard' }, - { id: 'connections', label: 'Connections', icon: RouteIcon, route: 'connections' }, - { id: 'logs', label: 'Email log', icon: ListIcon, route: 'logs' }, - { id: 'deliverability', label: 'Deliverability', icon: ShieldIcon, route: 'deliverability' }, - ], - }, - { - id: 'system', - footer: true, - items: [ { id: 'settings', label: 'Settings', icon: GearIcon, route: 'settings' } ], - }, + { label: 'Overview', route: 'dashboard' }, + { label: 'Connections', route: 'connections' }, + { label: 'Email log', route: 'logs' }, + { label: 'Deliverability', route: 'deliverability' }, + { label: 'Settings', route: 'settings' }, ]; const SKELETONS = { @@ -71,33 +60,20 @@ export default function App() { return () => window.removeEventListener( 'hashchange', handler ); }, [] ); - // Mirror the active route onto the WP admin submenu highlight for - // programmatic navigation (menu CLICKS are handled by the PHP-side - // interceptor). The submenu mirrors NAV, one entry per page. - useEffect( () => { - const seg = route.split( '/' )[ 0 ] || 'dashboard'; - const items = document.querySelectorAll( '#adminmenu .wp-submenu li' ); - items.forEach( ( li ) => { - const href = li.querySelector( 'a' )?.getAttribute( 'href' ) || ''; - const hit = href.includes( 'page=mailyard#/' + seg ) || ( 'dashboard' === seg && /page=mailyard$/.test( href ) ); - li.classList.toggle( 'current', hit ); - } ); - }, [ route ] ); - const navigate = useCallback( ( id ) => { window.location.hash = '#/' + id; }, [] ); const Skeleton = SKELETONS[ route.split( '/' )[ 0 ] ] || DashboardSkeleton; - // AppShell owns the sidebar frame (sticky rail, <782px icon-rail collapse) - // and pads the content column; the outlet only centers itself. + // AppShell owns the top bar (sticky under the admin bar) and pads the + // content column; the outlet only centers itself. return ( <> } + variant="topbar" + nav={