# 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'; // ... ``` In `auth/main.tsx`: ```tsx import { mailhubTheme } from '../theme'; // ... ``` 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: }, { key: 'domains', labelKey: 'nav.domains', icon: }, { key: 'dns-api', labelKey: 'nav.dnsApi', icon: } ] }, { key: 'delivery', labelKey: 'nav.group.delivery', items: [ { key: 'smtp', labelKey: 'nav.smtp', icon: }, { key: 'tokens', labelKey: 'nav.tokens', icon: }, { key: 'logs', labelKey: 'nav.logs', icon: }, { key: 'webhooks', labelKey: 'nav.webhooks', icon: } ] }, { key: 'system', labelKey: 'nav.group.system', items: [ { key: 'admin', labelKey: 'nav.admin', icon: , adminOnly: true }, { key: 'settings', labelKey: 'nav.settings', icon: } ] } ]; ``` 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 ( {icon} {children} ); } ``` ```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