Jelajahi Sumber

feat(ui): upgrade domains list and detail hero visuals

AI-Co-Authored-By: Grok
chendeben 1 bulan lalu
induk
melakukan
18b6fce6c2

+ 8 - 2
src/components/domain/AddDomainDrawer.tsx

@@ -61,10 +61,16 @@ export function AddDomainDrawer({
       open={open}
       onClose={onClose}
       destroyOnHidden
+      className="add-domain-drawer"
+      styles={{
+        header: { padding: '16px 24px' },
+        body: { padding: '20px 24px 28px' },
+        footer: { padding: '12px 24px' }
+      }}
       footer={
         <div className="drawer-footer">
           <Button onClick={onClose}>{t('common.cancel')}</Button>
-          <Space>
+          <Space size={8}>
             <Button disabled={current === 0} onClick={() => setCurrent((value) => value - 1)}>
               {t('common.previous')}
             </Button>
@@ -82,7 +88,7 @@ export function AddDomainDrawer({
       }
     >
       <Space direction="vertical" size={24} className="full-width">
-        <Steps current={current} items={steps.map((title) => ({ title }))} direction="vertical" responsive />
+        <Steps current={current} items={steps.map((title) => ({ title }))} direction="vertical" responsive className="add-domain-steps" />
         <Form form={form} layout="vertical" requiredMark="optional">
           <div hidden={current !== 0}>
             <Form.Item

+ 13 - 19
src/components/domain/DnsRecordCard.tsx

@@ -1,10 +1,12 @@
-import { CopyOutlined, ReloadOutlined } from '@ant-design/icons';
-import { Alert, Button, Card, Space, Tooltip, Typography } from 'antd';
+import { ReloadOutlined } from '@ant-design/icons';
+import { Alert, Button, Space, Typography } from 'antd';
 
-import { useI18n } from '../../frontend/i18n/react';
-import type { DnsRecord } from '../../frontend/types';
+import { CodeBlock } from '../common/CodeBlock';
+import { SectionCard } from '../common/SectionCard';
 import { StatusTag } from '../common/StatusTag';
 import { getDnsCurrentValues } from '../../frontend/domain-model.js';
+import { useI18n } from '../../frontend/i18n/react';
+import type { DnsRecord } from '../../frontend/types';
 
 interface DnsRecordCardProps {
   record: DnsRecord;
@@ -18,15 +20,15 @@ export function DnsRecordCard({ record, loading, onCopy, onRecheck }: DnsRecordC
   const currentValues: string[] = getDnsCurrentValues(record);
 
   return (
-    <Card
+    <SectionCard
       className="dns-record-card"
       title={
-        <Space wrap>
-          <span>{record.label}</span>
+        <Space wrap size={8}>
+          <span className="dns-record-card__label">{record.label}</span>
           <StatusTag record={record} />
         </Space>
       }
-      extra={<Typography.Text code>{record.type}</Typography.Text>}
+      extra={<Typography.Text code className="dns-record-card__type">{record.type}</Typography.Text>}
     >
       <Space direction="vertical" size={14} className="full-width">
         <DnsValueRow label={t('dnsRecord.hostname')} value={record.host} onCopy={onCopy} />
@@ -36,9 +38,7 @@ export function DnsRecordCard({ record, loading, onCopy, onRecheck }: DnsRecordC
           {currentValues.length ? (
             <Space direction="vertical" size={8} className="full-width value-stack">
               {currentValues.map((value) => (
-                <Typography.Paragraph key={value} code copyable className="dns-code-block">
-                  {value}
-                </Typography.Paragraph>
+                <CodeBlock key={value} value={value} onCopy={onCopy} />
               ))}
             </Space>
           ) : (
@@ -61,7 +61,7 @@ export function DnsRecordCard({ record, loading, onCopy, onRecheck }: DnsRecordC
           {t('dnsRecord.recheck')}
         </Button>
       </Space>
-    </Card>
+    </SectionCard>
   );
 }
 
@@ -74,16 +74,10 @@ function DnsValueRow({
   value: string;
   onCopy: (value: string) => void;
 }) {
-  const { t } = useI18n();
   return (
     <div className="dns-value-row">
       <Typography.Text type="secondary">{label}</Typography.Text>
-      <Typography.Paragraph code copyable className="dns-code-block">
-        {value}
-      </Typography.Paragraph>
-      <Tooltip title={`${t('dnsRecord.copyLabel')}${label}`}>
-        <Button icon={<CopyOutlined />} onClick={() => onCopy(value)} aria-label={`${t('dnsRecord.copyLabel')}${label}`} />
-      </Tooltip>
+      <CodeBlock value={value} onCopy={onCopy} />
     </div>
   );
 }

+ 49 - 27
src/components/domain/DomainHealthCard.tsx

@@ -1,6 +1,7 @@
 import { CheckCircleOutlined, ClockCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
-import { Button, Card, Col, Progress, Row, Space, Statistic, Tag, Typography } from 'antd';
+import { Button, Col, Progress, Row, Space, Typography } from 'antd';
 
+import { StatusPill } from '../common/StatusPill';
 import { buildDomainHealth } from '../../frontend/domain-model.js';
 import { useI18n } from '../../frontend/i18n/react';
 import type { Domain } from '../../frontend/types';
@@ -28,6 +29,7 @@ export function DomainHealthCard({
 }: DomainHealthCardProps) {
   const { t } = useI18n();
   const health = buildDomainHealth(domain);
+  const tone = health.status === 'success' ? 'success' : health.status === 'warning' ? 'warning' : 'error';
   const icon = health.status === 'success'
     ? <CheckCircleOutlined />
     : health.status === 'warning'
@@ -35,33 +37,31 @@ export function DomainHealthCard({
       : <ExclamationCircleOutlined />;
 
   return (
-    <Card className="domain-health-card">
+    <div className="domain-health-card">
       <Row gutter={[24, 24]} align="middle">
         <Col xs={24} xl={16}>
-          <Space direction="vertical" size={16} className="full-width">
+          <Space direction="vertical" size={20} className="full-width">
             <div className="domain-title-row">
-              <div>
-                <Typography.Text type="secondary">{t('domainHealth.sendingDomain')}</Typography.Text>
-                <Typography.Title level={2}>{domain.domain}</Typography.Title>
+              <div className="domain-title-row__text">
+                <Typography.Text type="secondary" className="domain-health-card__eyebrow">
+                  {t('domainHealth.sendingDomain')}
+                </Typography.Text>
+                <Typography.Title level={2} className="domain-health-card__title">
+                  {domain.domain}
+                </Typography.Title>
               </div>
-              <Tag color={health.status === 'success' ? 'success' : health.status === 'warning' ? 'warning' : 'error'} icon={icon}>
+              <StatusPill tone={tone} icon={icon}>
                 {domainHealthLabel(health.status, t)}
-              </Tag>
+              </StatusPill>
             </div>
-            <Row gutter={[16, 16]}>
-              <Col xs={12} md={6}>
-                <Statistic title={t('domains.senderHost')} value={domain.senderHost || '-'} valueStyle={{ fontSize: 14 }} />
-              </Col>
-              <Col xs={12} md={6}>
-                <Statistic title={t('domains.sendingIp')} value={domain.sendingIp || '-'} valueStyle={{ fontSize: 14 }} />
-              </Col>
-              <Col xs={12} md={6}>
-                <Statistic title="DKIM selector" value={domain.selector} valueStyle={{ fontSize: 14 }} />
-              </Col>
-              <Col xs={12} md={6}>
-                <Statistic title={t('domains.lastSent')} value={lastSentAt || t('common.notFound')} valueStyle={{ fontSize: 14 }} />
-              </Col>
-            </Row>
+
+            <div className="domain-health-stats">
+              <HealthStat label={t('domains.senderHost')} value={domain.senderHost || '-'} />
+              <HealthStat label={t('domains.sendingIp')} value={domain.sendingIp || '-'} />
+              <HealthStat label="DKIM selector" value={domain.selector} />
+              <HealthStat label={t('domains.lastSent')} value={lastSentAt || t('common.notFound')} />
+            </div>
+
             <div className="health-progress">
               <div>
                 <Typography.Text strong>{t('domainHealth.dnsProgress')}</Typography.Text>
@@ -69,11 +69,20 @@ export function DomainHealthCard({
                   {health.passed}/{health.total} {t('domainHealth.passed')} · {t('domainHealth.dnsIssues')} {health.dnsIssues}
                 </Typography.Text>
               </div>
-              <Progress percent={health.percent} status={health.status === 'error' ? 'exception' : 'active'} />
+              <Progress
+                percent={health.percent}
+                status={health.status === 'error' ? 'exception' : health.status === 'success' ? 'success' : 'active'}
+                strokeColor={health.status === 'success' ? 'var(--mh-success)' : health.status === 'error' ? 'var(--mh-danger)' : 'var(--mh-primary)'}
+              />
             </div>
-            <Space wrap>
-              <Tag>{t('domainHealth.dnsApi')}:{dnsApiName || t('common.notConfigured')}</Tag>
-              <Tag>{t('domainDetail.lastCheck')}:{health.checkedAt ? new Date(health.checkedAt).toLocaleString() : t('domainDetail.notChecked')}</Tag>
+
+            <Space wrap size={8} className="domain-health-meta">
+              <StatusPill tone={dnsApiName ? 'info' : 'neutral'}>
+                {t('domainHealth.dnsApi')}:{dnsApiName || t('common.notConfigured')}
+              </StatusPill>
+              <StatusPill tone={health.checkedAt ? 'neutral' : 'warning'}>
+                {t('domainDetail.lastCheck')}:{health.checkedAt ? new Date(health.checkedAt).toLocaleString() : t('domainDetail.notChecked')}
+              </StatusPill>
             </Space>
           </Space>
         </Col>
@@ -94,7 +103,20 @@ export function DomainHealthCard({
           </div>
         </Col>
       </Row>
-    </Card>
+    </div>
+  );
+}
+
+function HealthStat({ label, value }: { label: string; value: string }) {
+  return (
+    <div className="domain-health-stat">
+      <Typography.Text type="secondary" className="domain-health-stat__label">
+        {label}
+      </Typography.Text>
+      <Typography.Text strong className="domain-health-stat__value">
+        {value}
+      </Typography.Text>
+    </div>
   );
 }
 

+ 91 - 8
src/frontend/styles.css

@@ -318,8 +318,16 @@ body {
   padding: 0;
 }
 
-.domain-health-card .ant-card-body {
-  padding: 24px;
+.domains-toolbar-card .ant-card-body {
+  padding: 16px 20px;
+}
+
+.domain-health-card {
+  background: var(--mh-surface);
+  border: 1px solid var(--mh-border);
+  border-radius: var(--mh-radius-card);
+  box-shadow: var(--mh-shadow-card);
+  padding: 28px;
 }
 
 .domain-title-row {
@@ -329,8 +337,46 @@ body {
   justify-content: space-between;
 }
 
-.domain-title-row h2 {
-  margin: 4px 0 0 !important;
+.domain-title-row__text {
+  min-width: 0;
+}
+
+.domain-health-card__eyebrow {
+  display: block;
+  font-size: 12px;
+  font-weight: 600;
+  letter-spacing: 0.04em;
+  text-transform: uppercase;
+}
+
+.domain-health-card__title {
+  margin: 6px 0 0 !important;
+  overflow-wrap: anywhere;
+}
+
+.domain-health-stats {
+  display: grid;
+  gap: 12px;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.domain-health-stat {
+  background: #f8fafc;
+  border: 1px solid var(--mh-border);
+  border-radius: 12px;
+  display: grid;
+  gap: 4px;
+  min-width: 0;
+  padding: 12px 14px;
+}
+
+.domain-health-stat__label {
+  font-size: 12px;
+}
+
+.domain-health-stat__value {
+  display: block;
+  font-size: 14px;
   overflow-wrap: anywhere;
 }
 
@@ -342,26 +388,42 @@ body {
 .health-progress > div {
   align-items: center;
   display: flex;
+  gap: 12px;
   justify-content: space-between;
 }
 
+.domain-health-meta {
+  margin-top: 2px;
+}
+
 .domain-action-panel {
-  background: #f8fafc;
+  background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
   border: 1px solid var(--mh-border);
-  border-radius: 12px;
+  border-radius: 14px;
   display: grid;
   gap: 12px;
-  padding: 16px;
+  padding: 18px;
 }
 
 .dns-record-card .ant-card-head-title {
   min-width: 0;
 }
 
+.dns-record-card__label {
+  font-weight: 600;
+}
+
+.dns-record-card__type {
+  font-size: 12px;
+}
+
 .dns-value-row {
   display: grid;
   gap: 8px;
-  grid-template-columns: 72px minmax(0, 1fr) 40px;
+}
+
+.dns-value-row .code-block {
+  width: 100%;
 }
 
 .dns-code-block {
@@ -381,6 +443,18 @@ body {
   margin-top: 8px;
 }
 
+.domain-detail-tabs .ant-tabs-nav {
+  margin-bottom: 16px;
+}
+
+.domain-danger-card {
+  border-color: #fecaca;
+}
+
+.add-domain-drawer .add-domain-steps {
+  margin-bottom: 4px;
+}
+
 .code-sample {
   display: block;
   max-width: 100%;
@@ -413,6 +487,7 @@ body {
 .drawer-footer {
   align-items: center;
   display: flex;
+  gap: 12px;
   justify-content: space-between;
 }
 
@@ -719,6 +794,14 @@ body {
     width: 100%;
   }
 
+  .domain-health-stats {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .domain-health-card {
+    padding: 20px;
+  }
+
   .form-grid.two,
   .form-grid.three {
     grid-template-columns: 1fr;

+ 60 - 41
src/pages/Domains/DomainDetail.tsx

@@ -2,25 +2,26 @@ import { ArrowLeftOutlined, CopyOutlined } from '@ant-design/icons';
 import {
   Alert,
   Button,
-  Card,
-  Col,
   Collapse,
   Descriptions,
-  Empty,
   Form,
   Input,
   Modal,
   Row,
+  Col,
   Select,
   Space,
   Table,
   Tabs,
-  Tag,
   Typography
 } from 'antd';
 import type { ColumnsType } from 'antd/es/table';
 import { useMemo, useState } from 'react';
 
+import { CodeBlock } from '../../components/common/CodeBlock';
+import { EmptyState } from '../../components/common/EmptyState';
+import { SectionCard } from '../../components/common/SectionCard';
+import { StatusPill } from '../../components/common/StatusPill';
 import { DomainHealthCard } from '../../components/domain/DomainHealthCard';
 import { DnsRecordCard } from '../../components/domain/DnsRecordCard';
 import { getVisibleDnsRecords } from '../../frontend/domain-model.js';
@@ -108,7 +109,7 @@ export default function DomainDetail({
   }
 
   return (
-    <Space direction="vertical" size={16} className="full-width">
+    <Space direction="vertical" size={20} className="full-width">
       <Button icon={<ArrowLeftOutlined />} onClick={onBack}>
         {t('domainDetail.back')}
       </Button>
@@ -125,6 +126,7 @@ export default function DomainDetail({
       <Tabs
         activeKey={activeTab}
         onChange={setActiveTab}
+        className="domain-detail-tabs"
         items={[
           { key: 'overview', label: 'Overview', children: <OverviewTab domain={domain} events={domainEvents} smtpRelayName={smtpRelayName} onDelete={() => onDelete(domain)} /> },
           {
@@ -219,7 +221,7 @@ function OverviewTab({
   return (
     <Row gutter={[16, 16]}>
       <Col xs={24} lg={16}>
-        <Card title={t('domainDetail.overview')}>
+        <SectionCard title={t('domainDetail.overview')}>
           <Descriptions column={1}>
             <Descriptions.Item label={t('domains.domain')}>{domain.domain}</Descriptions.Item>
             <Descriptions.Item label={t('domains.senderHost')}>{domain.senderHost}</Descriptions.Item>
@@ -228,17 +230,17 @@ function OverviewTab({
             <Descriptions.Item label="DKIM selector">{domain.selector}</Descriptions.Item>
             <Descriptions.Item label={t('domains.lastSent')}>{events[0] ? new Date(events[0].createdAt).toLocaleString() : t('common.notFound')}</Descriptions.Item>
           </Descriptions>
-        </Card>
+        </SectionCard>
       </Col>
       <Col xs={24} lg={8}>
-        <Card title={t('domainDetail.danger')}>
-          <Space direction="vertical" className="full-width">
+        <SectionCard title={t('domainDetail.danger')} className="domain-danger-card">
+          <Space direction="vertical" className="full-width" size={12}>
             <Typography.Text type="secondary">{t('domainDetail.deleteHint')}</Typography.Text>
             <Button danger block onClick={onDelete}>
               {t('common.delete')}
             </Button>
           </Space>
-        </Card>
+        </SectionCard>
       </Col>
     </Row>
   );
@@ -268,16 +270,19 @@ function DnsRecordsTab({
           {records.length ? records.map((record) => (
               <DnsRecordCard key={record.key} record={record} loading={loading} onCopy={onCopy} onRecheck={onCheck} />
           )) : (
-            <Card>
-              <Empty description={t('domainDetail.noDnsResult')}>
-                <Button type="primary" onClick={onCheck}>{t('domainHealth.checkNow')}</Button>
-              </Empty>
-            </Card>
+            <SectionCard>
+              <EmptyState
+                description={t('domainDetail.noDnsResult')}
+                action={
+                  <Button type="primary" onClick={onCheck}>{t('domainHealth.checkNow')}</Button>
+                }
+              />
+            </SectionCard>
           )}
         </Space>
       </Col>
       <Col xs={24} xl={9}>
-        <Card
+        <SectionCard
           title={t('domainDetail.currentDnsResult')}
           extra={
             <Space>
@@ -289,18 +294,18 @@ function DnsRecordsTab({
           }
         >
           <Space direction="vertical" size={16} className="full-width">
-            <Tag>{t('domainDetail.lastCheck')}:{domain.status?.checkedAt ? new Date(domain.status.checkedAt).toLocaleString() : t('domainDetail.notChecked')}</Tag>
+            <StatusPill tone={domain.status?.checkedAt ? 'neutral' : 'warning'}>
+              {t('domainDetail.lastCheck')}:{domain.status?.checkedAt ? new Date(domain.status.checkedAt).toLocaleString() : t('domainDetail.notChecked')}
+            </StatusPill>
             {liveEntries.length ? (
               <Collapse
                 items={liveEntries.map(([key, values]) => ({
                   key,
                   label: liveLabel(key, t),
                   children: values.length ? (
-                    <Space direction="vertical" className="full-width">
+                    <Space direction="vertical" className="full-width" size={8}>
                       {values.map((value) => (
-                        <Typography.Paragraph key={value} code copyable className="dns-code-block">
-                          {value}
-                        </Typography.Paragraph>
+                        <CodeBlock key={value} value={value} onCopy={onCopy} />
                       ))}
                     </Space>
                   ) : (
@@ -309,13 +314,13 @@ function DnsRecordsTab({
                 }))}
               />
             ) : (
-              <Empty description={t('domainDetail.noPublicDns')} />
+              <EmptyState description={t('domainDetail.noPublicDns')} />
             )}
             {domain.status?.warnings?.length ? (
               <Alert type="warning" showIcon message={t('domainDetail.needAttention')} description={domain.status.warnings.join('\n')} />
             ) : null}
           </Space>
-        </Card>
+        </SectionCard>
       </Col>
     </Row>
   );
@@ -339,26 +344,32 @@ function SmtpApiTab({
   return (
     <Row gutter={[16, 16]}>
       <Col xs={24} lg={12}>
-        <Card title="SMTP">
+        <SectionCard title="SMTP">
           <Descriptions column={1}>
             <Descriptions.Item label="SMTP Host">{copyable(config?.submission?.host || '-', onCopy)}</Descriptions.Item>
             <Descriptions.Item label="SMTP Port">
-              {(config?.submission?.ports || []).map((item) => <Tag key={item.port}>{item.port} · {item.protocol}</Tag>)}
+              <Space wrap size={6}>
+                {(config?.submission?.ports || []).map((item) => (
+                  <StatusPill key={item.port} tone="info">
+                    {item.port} · {item.protocol}
+                  </StatusPill>
+                ))}
+              </Space>
             </Descriptions.Item>
             <Descriptions.Item label="Username">{copyable(smtpCredential?.username || config?.submission?.username || '-', onCopy)}</Descriptions.Item>
             <Descriptions.Item label="Password">{smtpCredential?.password ? copyable(smtpCredential.password, onCopy) : t('domainDetail.noSmtpPassword')}</Descriptions.Item>
             <Descriptions.Item label="TLS / SSL">{config?.submission?.tls ? 'TLS' : 'STARTTLS'}</Descriptions.Item>
           </Descriptions>
-        </Card>
+        </SectionCard>
       </Col>
       <Col xs={24} lg={12}>
-        <Card title="API">
+        <SectionCard title="API">
           <Descriptions column={1}>
             <Descriptions.Item label="API Endpoint">{copyable(apiEndpoint, onCopy)}</Descriptions.Item>
             <Descriptions.Item label="API Token">{apiTokens[0] ? `${apiTokens[0].tokenPrefix}...` : t('domainDetail.noApiToken')}</Descriptions.Item>
             <Descriptions.Item label="From">noreply@{domain.domain}</Descriptions.Item>
           </Descriptions>
-        </Card>
+        </SectionCard>
       </Col>
     </Row>
   );
@@ -370,10 +381,20 @@ function SendingLogsTab({ events }: { events: SendEvent[] }) {
     { title: t('logs.time'), dataIndex: 'createdAt', render: (value) => new Date(value).toLocaleString() },
     { title: t('logs.recipient'), dataIndex: 'recipients', render: (value: string[]) => value.join(', ') },
     { title: 'Subject', dataIndex: 'subject', ellipsis: true },
-    { title: t('common.status'), dataIndex: 'status', render: (value) => <Tag color={sendStatusColor(value)}>{sendStatusLabel(value, t)}</Tag> },
+    {
+      title: t('common.status'),
+      dataIndex: 'status',
+      render: (value) => (
+        <StatusPill tone={sendStatusTone(value)}>{sendStatusLabel(value, t)}</StatusPill>
+      )
+    },
     { title: t('logs.errorReason'), dataIndex: 'detail', ellipsis: true }
   ];
-  return <Table rowKey="id" columns={columns} dataSource={events} scroll={{ x: 900 }} />;
+  return (
+    <SectionCard title={t('logs.title')}>
+      <Table rowKey="id" columns={columns} dataSource={events} scroll={{ x: 900 }} />
+    </SectionCard>
+  );
 }
 
 function IntegrationGuideTab({
@@ -398,20 +419,18 @@ function IntegrationGuideTab({
     "text": "Signed with DKIM and queued by MailHub."
   }'`;
   return (
-    <Card title={t('domainDetail.apiExample')}>
-      <Typography.Paragraph code copyable className="code-sample">
-        {code}
-      </Typography.Paragraph>
-    </Card>
+    <SectionCard title={t('domainDetail.apiExample')}>
+      <CodeBlock value={code} />
+    </SectionCard>
   );
 }
 
 function Placeholder({ title }: { title: string }) {
   const { t } = useI18n();
   return (
-    <Card>
-      <Empty description={`${title} ${t('domainDetail.placeholder')}`} />
-    </Card>
+    <SectionCard>
+      <EmptyState description={`${title} ${t('domainDetail.placeholder')}`} />
+    </SectionCard>
   );
 }
 
@@ -428,12 +447,12 @@ function sendStatusLabel(status: string, t: (key: string) => string) {
   return status || t('dashboard.statusUnknown');
 }
 
-function sendStatusColor(status: string) {
-  if (status === 'queued') return 'processing';
+function sendStatusTone(status: string): 'success' | 'warning' | 'error' | 'info' | 'neutral' {
+  if (status === 'queued') return 'info';
   if (status === 'sent') return 'success';
   if (status === 'deferred') return 'warning';
   if (status === 'bounced' || status === 'failed') return 'error';
-  return 'default';
+  return 'neutral';
 }
 
 function liveLabel(key: string, t: (key: string) => string) {

+ 34 - 10
src/pages/Domains/index.tsx

@@ -1,12 +1,15 @@
 import { DeleteOutlined, EyeOutlined, SearchOutlined } from '@ant-design/icons';
-import { Button, Card, Input, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd';
+import { Button, Input, Popconfirm, Select, Space, Table, Typography } from 'antd';
 import type { ColumnsType } from 'antd/es/table';
 import { useMemo, useState } from 'react';
 
+import { PageHeader } from '../../components/common/PageHeader';
+import { SectionCard } from '../../components/common/SectionCard';
+import { StatusPill } from '../../components/common/StatusPill';
+import { StatusTag } from '../../components/common/StatusTag';
 import { buildDomainHealth } from '../../frontend/domain-model.js';
 import { useI18n } from '../../frontend/i18n/react';
 import type { DnsCredential, Domain, SendEvent } from '../../frontend/types';
-import { StatusTag } from '../../components/common/StatusTag';
 
 interface DomainsPageProps {
   domains: Domain[];
@@ -65,7 +68,11 @@ export default function DomainsPage({
       title: t('domains.dnsApi'),
       dataIndex: 'dnsCredentialId',
       width: 150,
-      render: (value: number | null) => value ? <Tag>{credentialName.get(value) || value}</Tag> : <Tag>{t('common.manual')}</Tag>
+      render: (value: number | null) => (
+        <StatusPill tone={value ? 'info' : 'neutral'}>
+          {value ? credentialName.get(value) || value : t('common.manual')}
+        </StatusPill>
+      )
     },
     recordColumn('DKIM', 'dkim'),
     recordColumn('SPF', 'spf'),
@@ -73,7 +80,11 @@ export default function DomainsPage({
     {
       title: t('domains.smtp'),
       width: 110,
-      render: (_, domain) => <Tag color={domain.status?.verified ? 'success' : 'warning'}>{domain.status?.verified ? t('domains.sendable') : t('domains.waitingVerify')}</Tag>
+      render: (_, domain) => (
+        <StatusPill tone={domain.status?.verified ? 'success' : 'warning'}>
+          {domain.status?.verified ? t('domains.sendable') : t('domains.waitingVerify')}
+        </StatusPill>
+      )
     },
     {
       title: t('domains.lastSent'),
@@ -88,7 +99,11 @@ export default function DomainsPage({
       width: 130,
       render: (_, domain) => {
         const health = buildDomainHealth(domain);
-        return <Tag color={health.status === 'success' ? 'success' : health.status === 'warning' ? 'warning' : 'error'}>{domainHealthLabel(health.status, t)}</Tag>;
+        return (
+          <StatusPill tone={healthTone(health.status)}>
+            {domainHealthLabel(health.status, t)}
+          </StatusPill>
+        );
       }
     },
     {
@@ -116,8 +131,10 @@ export default function DomainsPage({
   ];
 
   return (
-    <Space direction="vertical" size={16} className="full-width">
-      <Card>
+    <Space direction="vertical" size={20} className="full-width">
+      <PageHeader title={t('domains.title')} />
+
+      <SectionCard className="domains-toolbar-card">
         <div className="page-toolbar">
           <Space wrap>
             <Input
@@ -145,8 +162,9 @@ export default function DomainsPage({
             {t('common.addDomain')}
           </Button>
         </div>
-      </Card>
-      <Card
+      </SectionCard>
+
+      <SectionCard
         title={t('domains.title')}
         extra={
           <Typography.Text type="secondary">
@@ -155,7 +173,7 @@ export default function DomainsPage({
         }
       >
         <Table rowKey="id" columns={columns} dataSource={filtered} scroll={{ x: 1800 }} />
-      </Card>
+      </SectionCard>
     </Space>
   );
 }
@@ -172,6 +190,12 @@ function recordColumn(title: string, key: string): ColumnsType<Domain>[number] {
   };
 }
 
+function healthTone(status: string): 'success' | 'warning' | 'error' {
+  if (status === 'success') return 'success';
+  if (status === 'warning') return 'warning';
+  return 'error';
+}
+
 function domainHealthLabel(status: string, t: (key: string) => string) {
   if (status === 'success') return t('domains.healthy');
   if (status === 'warning') return t('domains.waitingDns');