|
|
@@ -0,0 +1,678 @@
|
|
|
+# 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
|