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 | 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 |
Files:
src/frontend/theme.tssrc/frontend/styles.css (top of file: :root tokens; keep existing class hooks, retarget colors)src/frontend/App.tsx (ConfigProvider theme import)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).
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.
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"
Files:
src/frontend/i18n/index.jsModify: 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');
});
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.)
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"
Files:
src/layouts/AdminLayout.tsxModify: 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).
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"
Files:
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:
var(--mh-ink)var(--mh-radius-card) (14px), var(--mh-shadow-card), comfortable paddingDo not change submit / fetch paths / mode state machine.
Retarget .auth-page, .auth-brand-panel, .auth-logo, .auth-signal-item, .auth-card, .auth-eyebrow to CSS variables and slightly larger radii.
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"
Files:
src/components/common/PageHeader.tsxsrc/components/common/MetricCard.tsxsrc/components/common/SectionCard.tsxsrc/components/common/StatusPill.tsxsrc/components/common/EmptyState.tsxsrc/components/common/CodeBlock.tsxsrc/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.
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.
Preserve badge mode behavior. Ensure getRecordStatusMeta mapping still works.
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"
Files:
src/pages/Dashboard.tsxModify: 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):
t('dashboard.todaySent') → summary.todayt('dashboard.successRate') → ${summary.successRate}% with hint showing bounce + complaint ratest('dashboard.verifiedDomains') → summary.verifiedDomainst('dashboard.dnsIssues') → summary.dnsIssues (warning tone when > 0)Secondary placement:
SectionCard extra/meta on recent logs cardKeep default-password Alert.
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"
Files:
src/pages/Domains/index.tsxsrc/pages/Domains/DomainDetail.tsxsrc/components/domain/DomainHealthCard.tsxsrc/components/domain/DnsRecordCard.tsxsrc/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.
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"
Files:
src/pages/SmtpCredentials.tsxsrc/pages/ApiTokens.tsxsrc/pages/SendingLogs.tsxsrc/pages/DnsApi.tsxsrc/pages/Settings.tsxsrc/pages/Admin/index.tsxsrc/pages/PlaceholderPage.tsxModify: src/frontend/styles.css as needed
[ ] Step 1: Apply PageHeader + SectionCard pattern to each page
Rules:
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"
Files:
Possibly minor CSS fixes only
[ ] Step 1: Run full test suite
Run: npm test
Expected: all tests PASS
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.)
src/server.js, models (except pure presentation helpers if unavoidable), or API contracts.Plan complete. Choose: