# 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**

```js
// 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`**

```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**

```css
: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:

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

In `auth/main.tsx`:

```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**

```bash
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):

```js
// 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**

```js
// 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**

```bash
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**

```tsx
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**

```css
.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**

```bash
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**

```bash
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**

```tsx
// 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>
  );
}
```

```css
.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:

```tsx
// 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**

```bash
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:

```ts
// 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**

```bash
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**

```bash
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**

```bash
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**

```bash
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
