2026-07-09-mailhub-ui-redesign.md 21 KB

MailHub UI Visual Redesign Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Upgrade MailHub admin and auth UIs to a Modern SaaS visual system (indigo primary, dark sidebar, comfortable density) without changing APIs or business logic.

Architecture: Extract a shared theme module used by both Vite entry points (App.tsx and auth/main.tsx). Redesign AdminLayout and auth shell, add shared presentation components under src/components/common/, then restyle pages by composition only. Keep handlers, models, and routes unchanged.

Tech Stack: React 19, Ant Design 5, Vite 8, TypeScript, existing i18n (src/frontend/i18n), Node test suite (npm test)

Spec: docs/superpowers/specs/2026-07-09-mailhub-ui-redesign-design.md


File map

File Responsibility
Create src/frontend/theme.ts Shared Ant Design theme config + exported color constants for plots/CSS alignment
Modify src/frontend/styles.css CSS variables, shell, cards, pills, auth, layout polish
Modify src/frontend/App.tsx Import shared theme into admin ConfigProvider
Modify src/frontend/auth/main.tsx Import shared theme into auth ConfigProvider
Modify src/layouts/AdminLayout.tsx Grouped nav, brand mark, header hierarchy, optional sider footer
Modify src/frontend/auth/AuthApp.tsx Auth shell markup/classes only (no auth API changes)
Create src/components/common/PageHeader.tsx Title / subtitle / actions
Create src/components/common/MetricCard.tsx Primary KPI card
Create src/components/common/SectionCard.tsx Standard elevated content card wrapper
Create src/components/common/StatusPill.tsx Soft semantic status chip
Create src/components/common/EmptyState.tsx Empty list/chart state
Create src/components/common/CodeBlock.tsx Wrap-safe mono value + optional copy
Modify src/components/common/StatusTag.tsx Prefer StatusPill styling (or thin wrapper)
Modify src/pages/Dashboard.tsx 4 MetricCards + secondary metrics + token chart colors
Modify src/pages/Domains/index.tsx PageHeader + SectionCard table chrome
Modify src/pages/Domains/DomainDetail.tsx Section wrappers / tab chrome
Modify src/components/domain/DomainHealthCard.tsx Hero health block
Modify src/components/domain/DnsRecordCard.tsx CodeBlock + StatusPill
Modify remaining pages PageHeader + SectionCard sweep
Modify src/frontend/i18n/index.js Nav group + minor chrome strings (zh-CN + en-US)
Modify test/frontend-i18n.test.js Assert new i18n keys if added
Create test/frontend-theme.test.js Assert primary token is indigo not Ant default blue

Task 1: Shared theme module + CSS variables

Files:

  • Create: src/frontend/theme.ts
  • Modify: src/frontend/styles.css (top of file: :root tokens; keep existing class hooks, retarget colors)
  • Modify: src/frontend/App.tsx (ConfigProvider theme import)
  • Modify: src/frontend/auth/main.tsx (ConfigProvider theme import)
  • Create: test/frontend-theme.test.js

  • [ ] Step 1: Write the failing theme test

    // test/frontend-theme.test.js
    import test from 'node:test';
    import assert from 'node:assert/strict';
    import { mailhubTheme, brandColors } from '../src/frontend/theme.ts';
    
    test('brand primary is indigo, not Ant Design default blue', () => {
    assert.equal(brandColors.primary, '#4F46E5');
    assert.notEqual(brandColors.primary.toLowerCase(), '#1677ff');
    assert.equal(mailhubTheme.token.colorPrimary, brandColors.primary);
    });
    
    test('layout canvas and ink tokens match redesign spec', () => {
    assert.equal(brandColors.canvas, '#F4F6FB');
    assert.equal(brandColors.ink, '#0F172A');
    assert.equal(mailhubTheme.token.colorBgLayout, brandColors.canvas);
    });
    
  • [ ] Step 2: Run test to verify it fails

Run: node --test test/frontend-theme.test.js
Expected: FAIL (module not found or exports missing)

  • [ ] Step 3: Implement src/frontend/theme.ts

    import type { ThemeConfig } from 'antd';
    
    export const brandColors = {
    primary: '#4F46E5',
    primaryHover: '#4338CA',
    primarySoft: '#EEF2FF',
    ink: '#0F172A',
    textSecondary: '#64748B',
    textMuted: '#94A3B8',
    canvas: '#F4F6FB',
    surface: '#FFFFFF',
    border: '#E2E8F0',
    success: '#16A34A',
    warning: '#D97706',
    danger: '#DC2626',
    chartPrimary: '#4F46E5',
    chartSuccess: '#16A34A',
    chartDanger: '#DC2626',
    chartWarning: '#D97706',
    chartTrack: '#DBEAFE'
    } as const;
    
    export const mailhubTheme: ThemeConfig = {
    token: {
    colorPrimary: brandColors.primary,
    colorSuccess: brandColors.success,
    colorWarning: brandColors.warning,
    colorError: brandColors.danger,
    colorBgLayout: brandColors.canvas,
    colorBgContainer: brandColors.surface,
    colorBorderSecondary: brandColors.border,
    colorText: brandColors.ink,
    colorTextSecondary: brandColors.textSecondary,
    borderRadius: 10,
    borderRadiusLG: 14,
    fontFamily:
      'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
    },
    components: {
    Card: {
      borderRadiusLG: 14
    },
    Table: {
      cellPaddingBlock: 14,
      cellPaddingInline: 16
    },
    Button: {
      controlHeight: 36,
      borderRadius: 10
    },
    Menu: {
      darkItemBg: brandColors.ink,
      darkSubMenuItemBg: brandColors.ink,
      darkItemSelectedBg: 'rgba(79, 70, 229, 0.22)',
      darkItemSelectedColor: '#E0E7FF',
      darkItemHoverBg: 'rgba(255, 255, 255, 0.06)',
      itemBorderRadius: 10
    }
    }
    };
    
  • [ ] Step 4: Add CSS variables at top of src/frontend/styles.css and retarget globals

    :root {
    --mh-primary: #4f46e5;
    --mh-primary-hover: #4338ca;
    --mh-primary-soft: #eef2ff;
    --mh-ink: #0f172a;
    --mh-text-secondary: #64748b;
    --mh-text-muted: #94a3b8;
    --mh-canvas: #f4f6fb;
    --mh-surface: #ffffff;
    --mh-border: #e2e8f0;
    --mh-success: #16a34a;
    --mh-warning: #d97706;
    --mh-danger: #dc2626;
    --mh-radius-control: 10px;
    --mh-radius-card: 14px;
    --mh-shadow-card: 0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.04);
    }
    
    body {
    margin: 0;
    background: var(--mh-canvas);
    color: var(--mh-ink);
    font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    }
    

Replace hard-coded #1677ff / #f5f7fb / #111827 shell colors with the CSS variables as you touch those rules (full shell polish in Task 3).

  • Step 5: Wire shared theme in both entry ConfigProviders

In App.tsx, replace inline theme object with:

import { mailhubTheme } from './theme';
// ...
<ConfigProvider theme={mailhubTheme}>

In auth/main.tsx:

import { mailhubTheme } from '../theme';
// ...
<ConfigProvider theme={mailhubTheme}>

Ensure auth still imports ../styles.css.

  • Step 6: Run theme test + full suite

Run: node --test test/frontend-theme.test.js && npm test
Expected: theme tests PASS; full suite PASS

  • [ ] Step 7: Commit

    git add src/frontend/theme.ts src/frontend/styles.css src/frontend/App.tsx src/frontend/auth/main.tsx test/frontend-theme.test.js
    git commit -m "feat(ui): add shared MailHub indigo theme tokens"
    

Task 2: i18n chrome strings for nav groups

Files:

  • Modify: src/frontend/i18n/index.js
  • Modify: test/frontend-i18n.test.js

  • [ ] Step 1: Extend i18n test for new keys

Add assertions (both locales via existing helpers if present):

// Use existing createTranslator helpers from test/frontend-i18n.test.js
// (not bare zh()/en() — those may not exist).
test('nav group chrome strings exist', () => {
  const zh = createTranslator('zh-CN');
  const en = createTranslator('en-US');
  assert.equal(zh('nav.group.overview'), '概览');
  assert.equal(zh('nav.group.delivery'), '投递');
  assert.equal(zh('nav.group.system'), '系统');
  assert.equal(en('nav.group.overview'), 'Overview');
  assert.equal(en('nav.group.delivery'), 'Delivery');
  assert.equal(en('nav.group.system'), 'System');
});
  • Step 2: Run test to verify it fails

Run: node --test test/frontend-i18n.test.js
Expected: FAIL missing keys

  • [ ] Step 3: Add keys to zh-CN and en-US message maps

    // zh-CN
    'nav.group.overview': '概览',
    'nav.group.delivery': '投递',
    'nav.group.system': '系统',
    
    // en-US
    'nav.group.overview': 'Overview',
    'nav.group.delivery': 'Delivery',
    'nav.group.system': 'System',
    

(Only add keys that do not already exist. Keep existing nav.* page labels.)

  • Step 4: Run i18n tests

Run: node --test test/frontend-i18n.test.js
Expected: PASS

  • [ ] Step 5: Commit

    git add src/frontend/i18n/index.js test/frontend-i18n.test.js
    git commit -m "feat(i18n): add sidebar nav group labels"
    

Task 3: AdminLayout shell redesign

Files:

  • Modify: src/layouts/AdminLayout.tsx
  • Modify: src/frontend/styles.css (.admin-layout, .admin-sider, .brand*, .admin-header, .admin-content, menu active styles)

  • [ ] Step 1: Restructure nav data with groups

    const navGroups: Array<{
    key: string;
    labelKey: string;
    items: Array<{ key: ViewKey; labelKey: string; icon: ReactNode; adminOnly?: boolean }>;
    }> = [
    {
    key: 'overview',
    labelKey: 'nav.group.overview',
    items: [
      { key: 'dashboard', labelKey: 'nav.dashboard', icon: <DashboardOutlined /> },
      { key: 'domains', labelKey: 'nav.domains', icon: <GlobalOutlined /> },
      { key: 'dns-api', labelKey: 'nav.dnsApi', icon: <CloudServerOutlined /> }
    ]
    },
    {
    key: 'delivery',
    labelKey: 'nav.group.delivery',
    items: [
      { key: 'smtp', labelKey: 'nav.smtp', icon: <MailOutlined /> },
      { key: 'tokens', labelKey: 'nav.tokens', icon: <KeyOutlined /> },
      { key: 'logs', labelKey: 'nav.logs', icon: <SendOutlined /> },
      { key: 'webhooks', labelKey: 'nav.webhooks', icon: <ApiOutlined /> }
    ]
    },
    {
    key: 'system',
    labelKey: 'nav.group.system',
    items: [
      { key: 'admin', labelKey: 'nav.admin', icon: <SafetyCertificateOutlined />, adminOnly: true },
      { key: 'settings', labelKey: 'nav.settings', icon: <SettingOutlined /> }
    ]
    }
    ];
    

Build Ant Design Menu items as group entries (type: 'group') filtering adminOnly when user?.role !== 'admin'.

  • [ ] Step 2: Upgrade brand + header markup

  • Brand logo: gradient indigo/violet background (brand-logo class), white MH text

  • Header left: keep breadcrumb; ensure title hierarchy readable at comfortable size

  • Header right: language, refresh (default), primary add-domain, user dropdown

  • Optional: compact user chip in sider footer (display only; logout stays in header dropdown)

  • [ ] Step 3: CSS for modern sider

    .admin-sider {
    background: var(--mh-ink) !important;
    border-right: 1px solid rgba(255, 255, 255, 0.06);
    /* sticky full-height behavior already present — keep it */
    }
    
    .brand-logo {
    background: linear-gradient(135deg, #6366f1, #8b5cf6);
    color: #fff;
    border-radius: 10px;
    }
    
    .admin-header {
    background: var(--mh-surface);
    border-bottom: 1px solid var(--mh-border);
    min-height: 64px;
    padding: 12px 24px;
    }
    
    .admin-content {
    background: var(--mh-canvas);
    padding: 28px 28px 40px;
    }
    
    .ant-card {
    border-color: var(--mh-border);
    box-shadow: var(--mh-shadow-card);
    }
    

Tune Menu dark styles so selected item matches indigo soft wash (override if Ant Menu tokens are insufficient).

  • Step 4: Manual visual smoke (dev)

Run: npm run dev:ui (and npm run dev if API needed)
Check: sidebar groups, active item color indigo-tint, header CTA primary indigo, no #1677ff flash.

  • [ ] Step 5: Commit

    git add src/layouts/AdminLayout.tsx src/frontend/styles.css
    git commit -m "feat(ui): redesign admin shell with grouped dark sidebar"
    

Task 4: Auth shell restyle

Files:

  • Modify: src/frontend/auth/AuthApp.tsx (classNames / structure only)
  • Modify: src/frontend/styles.css (.auth-* rules)

  • [ ] Step 1: Align auth brand panel with tokens

Keep split layout and modes. Update:

  • Brand panel bg → var(--mh-ink)
  • Logo → same gradient as admin
  • Form card → var(--mh-radius-card) (14px), var(--mh-shadow-card), comfortable padding
  • Primary button inherits theme primary
  • Soft right panel gradient using primary-soft + canvas

Do not change submit / fetch paths / mode state machine.

  • Step 2: CSS refresh for auth

Retarget .auth-page, .auth-brand-panel, .auth-logo, .auth-signal-item, .auth-card, .auth-eyebrow to CSS variables and slightly larger radii.

  • Step 3: Smoke login page

Run: open Vite login entry or built login.html
Expected: indigo primary, consistent brand with admin, forms still switch login/register

  • [ ] Step 4: Commit

    git add src/frontend/auth/AuthApp.tsx src/frontend/styles.css
    git commit -m "feat(ui): restyle auth shell to match admin brand"
    

Task 5: Shared presentation components

Files:

  • Create: src/components/common/PageHeader.tsx
  • Create: src/components/common/MetricCard.tsx
  • Create: src/components/common/SectionCard.tsx
  • Create: src/components/common/StatusPill.tsx
  • Create: src/components/common/EmptyState.tsx
  • Create: src/components/common/CodeBlock.tsx
  • Modify: src/components/common/StatusTag.tsx (render via StatusPill when mode === 'tag')
  • Modify: src/frontend/styles.css (component classes)

  • [ ] Step 1: Implement StatusPill + styles

    // StatusPill.tsx — tone: success | warning | error | info | neutral
    import { Typography } from 'antd';
    import type { ReactNode } from 'react';
    
    export type StatusTone = 'success' | 'warning' | 'error' | 'info' | 'neutral';
    
    export function StatusPill({ tone = 'neutral', icon, children }: {
    tone?: StatusTone;
    icon?: ReactNode;
    children: ReactNode;
    }) {
    return (
    <span className={`status-pill status-pill--${tone}`}>
      {icon}
      <span>{children}</span>
    </span>
    );
    }
    
    .status-pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    border-radius: 999px;
    padding: 3px 10px;
    font-size: 12px;
    font-weight: 600;
    border: 1px solid transparent;
    }
    .status-pill--success { background: #ecfdf5; color: #15803d; border-color: #bbf7d0; }
    .status-pill--warning { background: #fffbeb; color: #b45309; border-color: #fde68a; }
    .status-pill--error { background: #fef2f2; color: #b91c1c; border-color: #fecaca; }
    .status-pill--info { background: var(--mh-primary-soft); color: #4338ca; border-color: #c7d2fe; }
    .status-pill--neutral { background: #f8fafc; color: #475569; border-color: var(--mh-border); }
    

Map existing domain/record status colors to tones in StatusTag.

  • Step 2: Implement PageHeader, MetricCard, SectionCard, EmptyState, CodeBlock

Minimal APIs:

// PageHeader
{ title: ReactNode; subtitle?: ReactNode; extra?: ReactNode }

// MetricCard
{ label: ReactNode; value: ReactNode; hint?: ReactNode; tone?: 'default' | 'warning' | 'danger' }

// SectionCard — wrap antd Card with className="section-card" + consistent props
{ title?: ReactNode; extra?: ReactNode; children: ReactNode; className?: string }

// EmptyState
{ description: ReactNode; action?: ReactNode; icon?: ReactNode }

// CodeBlock
{ value: string; onCopy?: (value: string) => void }

Use Ant Design Card / Typography / Button underneath where helpful; keep components presentational.

  • Step 3: Point StatusTag tag mode at StatusPill

Preserve badge mode behavior. Ensure getRecordStatusMeta mapping still works.

  • Step 4: Typecheck

Run: npx tsc --noEmit
Expected: PASS

  • [ ] Step 5: Commit

    git add src/components/common src/frontend/styles.css
    git commit -m "feat(ui): add shared presentation components for redesign"
    

Task 6: Dashboard restyle

Files:

  • Modify: src/pages/Dashboard.tsx
  • Modify: src/frontend/styles.css (metric/chart helpers if needed)

  • [ ] Step 1: Replace 8 equal metric cards with 4 MetricCards

Primary cards (order fixed per spec):

  1. t('dashboard.todaySent')summary.today
  2. t('dashboard.successRate')${summary.successRate}% with hint showing bounce + complaint rates
  3. t('dashboard.verifiedDomains')summary.verifiedDomains
  4. t('dashboard.dnsIssues')summary.dnsIssues (warning tone when > 0)

Secondary placement:

  • SMTP status → compact tag/chip near PageHeader extra or above charts
  • Last sent → SectionCard extra/meta on recent logs card

Keep default-password Alert.

  • Step 2: Wrap charts and lists in SectionCard; recolor plots

Import brandColors from ../frontend/theme and recolor all plots:

// Area trend (total / accepted / failed series)
scale={{ color: { range: [brandColors.chartPrimary, brandColors.chartSuccess, brandColors.chartDanger] } }}
// Pie status distribution — map with brand success/danger/warning (no leftover Ant defaults)
// Bar domain ranking — single brand primary or soft indigo range
// Column hourly heatmap:
scale={{ color: { range: [brandColors.chartTrack, brandColors.chartPrimary] } }}

Retain panels: trend, status distribution, domain ranking, hourly heatmap, recent failures, domain health, recent logs.

  • [ ] Step 3: Use EmptyState where Empty was used for charts/lists (optional but preferred)

  • [ ] Step 4: Run tests

Run: npm test
Expected: PASS (analytics models unchanged)

  • [ ] Step 5: Commit

    git add src/pages/Dashboard.tsx src/frontend/styles.css
    git commit -m "feat(ui): restyle dashboard metrics and charts"
    

Task 7: Domains list + domain detail visual upgrade

Files:

  • Modify: src/pages/Domains/index.tsx
  • Modify: src/pages/Domains/DomainDetail.tsx
  • Modify: src/components/domain/DomainHealthCard.tsx
  • Modify: src/components/domain/DnsRecordCard.tsx
  • Modify: src/components/domain/AddDomainDrawer.tsx (light polish only: footer/header spacing)
  • Modify: src/frontend/styles.css (.domain-health-card, .dns-record-card, hero layout)

  • [ ] Step 1: Domains list — PageHeader + SectionCard

Toolbar (search, status filter, add) stays; wrap table in SectionCard; domain link keeps table-link emphasis; statuses use StatusTag/StatusPill.

  • [ ] Step 2: DomainHealthCard hero

  • Large domain title

  • StatusPill for health

  • Stats row (sender host, IP, selector, last sent)

  • Progress + DNS API / last check meta

  • Action stack in right column with primary/default buttons (handlers unchanged)

  • [ ] Step 3: DnsRecordCard

Use CodeBlock for target/current values; StatusPill for record status; keep copy/recheck actions.

  • Step 4: DomainDetail chrome

Tabs unchanged in structure; wrap major sections with SectionCard where it improves hierarchy without breaking two-column DNS layout.

  • [ ] Step 5: Commit

    git add src/pages/Domains src/components/domain src/frontend/styles.css
    git commit -m "feat(ui): upgrade domains list and detail hero visuals"
    

Task 8: Remaining pages sweep

Files:

  • Modify: src/pages/SmtpCredentials.tsx
  • Modify: src/pages/ApiTokens.tsx
  • Modify: src/pages/SendingLogs.tsx
  • Modify: src/pages/DnsApi.tsx
  • Modify: src/pages/Settings.tsx
  • Modify: src/pages/Admin/index.tsx
  • Modify: src/pages/PlaceholderPage.tsx
  • Modify: src/frontend/styles.css as needed

  • [ ] Step 1: Apply PageHeader + SectionCard pattern to each page

Rules:

  • Do not change form field names, payloads, or table column data indexes
  • One primary CTA per toolbar region
  • Token secret modal: stronger warning Alert styling only
  • Placeholder webhooks page: EmptyState with short description

  • [ ] Step 2: Grep for leftover default blue hardcodes in UI source

Run: rg -n "#1677ff|#f5f7fb" src --glob '!**/node_modules/**'
Expected: no brand-primary leftovers in frontend UI (tests may still mention old values only if intentional — update if any)

  • [ ] Step 3: Commit

    git add src/pages src/frontend/styles.css
    git commit -m "feat(ui): apply redesign chrome across remaining admin pages"
    

Task 9: Build, test, and final polish gate

Files:

  • Possibly minor CSS fixes only

  • [ ] Step 1: Run full test suite

Run: npm test
Expected: all tests PASS

  • Step 2: Production UI build

Run: npm run build
Expected: tsc --noEmit + Vite build succeed; assets written under public/assets/

  • [ ] Step 3: Manual checklist

  • [ ] Login page: indigo primary, brand panel ink, form works

  • [ ] Admin shell: grouped nav, active state, sticky sider

  • [ ] Dashboard: 4 primary metrics; secondary metrics present; charts colored

  • [ ] Domains list + detail: hero + DNS cards readable

  • [ ] SMTP / Tokens / Logs / Settings load without layout break

  • [ ] Mobile width: sider collapses; auth stacks

  • [ ] Step 4: Final commit if polish remains

    git add -A
    git commit -m "fix(ui): polish redesign spacing and build assets"
    

(Only if there are changes.)


Notes for implementers

  • No CDN fonts. System font stack only unless product later self-hosts Inter.
  • Do not change src/server.js, models (except pure presentation helpers if unavoidable), or API contracts.
  • Prefer small commits per task above.
  • If a page is too large, change markup/classNames first; avoid drive-by refactors.
  • After implementation, prefer @superpowers:verification-before-completion before claiming done.

Execution handoff

Plan complete. Choose:

  1. Subagent-Driven (recommended) — fresh subagent per task + review between tasks
  2. Inline Execution — execute tasks in this session with checkpoints