mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
refactor(emailclient): extract MailboxHeader, MailboxSidebar, EmailDetail components
Moves inline JSX from EmailClientContent into proper FakeMUI email components in components/fakemui/email/layout/. All styling uses M3 CSS custom properties (--mat-sys-*) from the FakeMUI token system. New components: - MailboxHeader: top bar with search, avatar, settings - MailboxSidebar: compose button + folder navigation - EmailDetail: reading pane with toolbar, header, body, reply bar Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,13 @@ export {
|
||||
export {
|
||||
MailboxLayout,
|
||||
type MailboxLayoutProps,
|
||||
MailboxHeader,
|
||||
type MailboxHeaderProps,
|
||||
MailboxSidebar,
|
||||
type MailboxSidebarProps,
|
||||
EmailDetail,
|
||||
type EmailDetailProps,
|
||||
type EmailDetailEmail,
|
||||
ComposerLayout,
|
||||
type ComposerLayoutProps,
|
||||
SettingsLayout,
|
||||
|
||||
109
components/fakemui/email/layout/EmailDetail.tsx
Normal file
109
components/fakemui/email/layout/EmailDetail.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React from 'react'
|
||||
import { Box, BoxProps, Button, IconButton } from '../..'
|
||||
import { useAccessible } from '../../../../hooks/useAccessible'
|
||||
import { EmailHeader } from '../data-display'
|
||||
|
||||
export interface EmailDetailEmail {
|
||||
id: string
|
||||
from: string
|
||||
to: string[]
|
||||
cc?: string[]
|
||||
subject: string
|
||||
body: string
|
||||
receivedAt: number
|
||||
isStarred: boolean
|
||||
}
|
||||
|
||||
export interface EmailDetailProps extends BoxProps {
|
||||
email: EmailDetailEmail
|
||||
onClose?: () => void
|
||||
onArchive?: () => void
|
||||
onDelete?: () => void
|
||||
onReply?: () => void
|
||||
onForward?: () => void
|
||||
onToggleStar?: (starred: boolean) => void
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const EmailDetail = ({
|
||||
email,
|
||||
onClose,
|
||||
onArchive,
|
||||
onDelete,
|
||||
onReply,
|
||||
onForward,
|
||||
onToggleStar,
|
||||
testId: customTestId,
|
||||
...props
|
||||
}: EmailDetailProps) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'email-detail',
|
||||
identifier: customTestId || 'detail'
|
||||
})
|
||||
|
||||
return (
|
||||
<Box className="email-detail" {...accessible} {...props}>
|
||||
<Box className="email-detail-toolbar">
|
||||
{onClose && (
|
||||
<IconButton
|
||||
aria-label="Back to list"
|
||||
className="email-detail-back"
|
||||
onClick={onClose}
|
||||
>
|
||||
<span>←</span>
|
||||
</IconButton>
|
||||
)}
|
||||
<Box className="email-detail-actions">
|
||||
{onArchive && (
|
||||
<IconButton aria-label="Archive" title="Archive" onClick={onArchive}>
|
||||
<span>📥</span>
|
||||
</IconButton>
|
||||
)}
|
||||
{onDelete && (
|
||||
<IconButton aria-label="Delete" title="Delete" onClick={onDelete}>
|
||||
<span>🗑️</span>
|
||||
</IconButton>
|
||||
)}
|
||||
{onReply && (
|
||||
<IconButton aria-label="Reply" title="Reply" onClick={onReply}>
|
||||
<span>↩️</span>
|
||||
</IconButton>
|
||||
)}
|
||||
{onForward && (
|
||||
<IconButton aria-label="Forward" title="Forward" onClick={onForward}>
|
||||
<span>↪️</span>
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<EmailHeader
|
||||
from={email.from}
|
||||
to={email.to}
|
||||
cc={email.cc}
|
||||
subject={email.subject}
|
||||
receivedAt={email.receivedAt}
|
||||
isStarred={email.isStarred}
|
||||
onToggleStar={onToggleStar}
|
||||
/>
|
||||
|
||||
<Box className="email-detail-body">
|
||||
{email.body}
|
||||
</Box>
|
||||
|
||||
<Box className="email-detail-reply-bar">
|
||||
{onReply && (
|
||||
<Button variant="outlined" className="email-detail-reply-btn" onClick={onReply}>
|
||||
↩️ Reply
|
||||
</Button>
|
||||
)}
|
||||
{onForward && (
|
||||
<Button variant="outlined" className="email-detail-reply-btn" onClick={onForward}>
|
||||
↪️ Forward
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
63
components/fakemui/email/layout/MailboxHeader.tsx
Normal file
63
components/fakemui/email/layout/MailboxHeader.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react'
|
||||
import { Box, BoxProps, IconButton, Typography } from '../..'
|
||||
import { useAccessible } from '../../../../hooks/useAccessible'
|
||||
|
||||
export interface MailboxHeaderProps extends BoxProps {
|
||||
appName?: string
|
||||
appIcon?: string
|
||||
searchQuery?: string
|
||||
onSearchChange?: (query: string) => void
|
||||
searchPlaceholder?: string
|
||||
avatarLabel?: string
|
||||
onSettingsClick?: () => void
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const MailboxHeader = ({
|
||||
appName = 'MetaMail',
|
||||
appIcon = '📧',
|
||||
searchQuery = '',
|
||||
onSearchChange,
|
||||
searchPlaceholder = 'Search mail',
|
||||
avatarLabel = 'U',
|
||||
onSettingsClick,
|
||||
testId: customTestId,
|
||||
...props
|
||||
}: MailboxHeaderProps) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'mailbox-header',
|
||||
identifier: customTestId || 'header'
|
||||
})
|
||||
|
||||
return (
|
||||
<Box className="mailbox-header-bar" {...accessible} {...props}>
|
||||
<Box className="mailbox-header-brand">
|
||||
<span className="mailbox-header-icon">{appIcon}</span>
|
||||
<Typography variant="h6" className="mailbox-header-title">
|
||||
{appName}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box className="mailbox-header-search">
|
||||
<input
|
||||
type="search"
|
||||
className="mailbox-search-input"
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchQuery}
|
||||
onChange={e => onSearchChange?.(e.target.value)}
|
||||
aria-label={searchPlaceholder}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="mailbox-header-actions">
|
||||
{onSettingsClick && (
|
||||
<IconButton aria-label="Settings" title="Settings" onClick={onSettingsClick}>
|
||||
<span className="mailbox-header-action-icon">⚙️</span>
|
||||
</IconButton>
|
||||
)}
|
||||
<Box className="mailbox-header-avatar" aria-label="Account">
|
||||
{avatarLabel}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
40
components/fakemui/email/layout/MailboxSidebar.tsx
Normal file
40
components/fakemui/email/layout/MailboxSidebar.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
import { Box, BoxProps } from '../..'
|
||||
import { useAccessible } from '../../../../hooks/useAccessible'
|
||||
import { FolderNavigation, type FolderNavigationItem } from '../navigation'
|
||||
|
||||
export interface MailboxSidebarProps extends BoxProps {
|
||||
folders: FolderNavigationItem[]
|
||||
onNavigate?: (folderId: string) => void
|
||||
onCompose?: () => void
|
||||
composeLabel?: string
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const MailboxSidebar = ({
|
||||
folders,
|
||||
onNavigate,
|
||||
onCompose,
|
||||
composeLabel = 'Compose',
|
||||
testId: customTestId,
|
||||
...props
|
||||
}: MailboxSidebarProps) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'mailbox-sidebar',
|
||||
identifier: customTestId || 'sidebar'
|
||||
})
|
||||
|
||||
return (
|
||||
<Box className="mailbox-sidebar-content" {...accessible} {...props}>
|
||||
{onCompose && (
|
||||
<Box className="mailbox-sidebar-compose">
|
||||
<button className="compose-btn" onClick={onCompose}>
|
||||
✏️ {composeLabel}
|
||||
</button>
|
||||
</Box>
|
||||
)}
|
||||
<FolderNavigation items={folders} onNavigate={onNavigate} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
export { MailboxLayout, type MailboxLayoutProps } from './MailboxLayout'
|
||||
export { MailboxHeader, type MailboxHeaderProps } from './MailboxHeader'
|
||||
export { MailboxSidebar, type MailboxSidebarProps } from './MailboxSidebar'
|
||||
export { EmailDetail, type EmailDetailProps, type EmailDetailEmail } from './EmailDetail'
|
||||
export { ComposerLayout, type ComposerLayoutProps } from './ComposerLayout'
|
||||
export { SettingsLayout, type SettingsLayoutProps, type SettingsSection } from './SettingsLayout'
|
||||
|
||||
@@ -3,18 +3,14 @@
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import {
|
||||
MailboxLayout,
|
||||
FolderNavigation,
|
||||
MailboxHeader,
|
||||
MailboxSidebar,
|
||||
EmailDetail,
|
||||
type FolderNavigationItem,
|
||||
ThreadList,
|
||||
EmailHeader,
|
||||
ComposeWindow,
|
||||
} from '@metabuilder/fakemui/email'
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
IconButton,
|
||||
Button,
|
||||
} from '@metabuilder/fakemui'
|
||||
import { Box, Typography } from '@metabuilder/fakemui'
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Demo data — replace with useMessages/useMailboxes hooks when IMAP backend is ready
|
||||
@@ -133,7 +129,6 @@ export default function EmailClientContent() {
|
||||
if (activeFolder === 'drafts') return false
|
||||
if (activeFolder === 'spam') return false
|
||||
if (activeFolder === 'trash') return false
|
||||
// inbox
|
||||
if (searchQuery) {
|
||||
const q = searchQuery.toLowerCase()
|
||||
return (
|
||||
@@ -147,7 +142,6 @@ export default function EmailClientContent() {
|
||||
|
||||
const handleSelectEmail = useCallback((emailId: string) => {
|
||||
setSelectedEmailId(emailId)
|
||||
// Mark as read
|
||||
setEmails(prev => prev.map(e => e.id === emailId ? { ...e, isRead: true } : e))
|
||||
}, [])
|
||||
|
||||
@@ -162,6 +156,7 @@ export default function EmailClientContent() {
|
||||
const handleSend = useCallback((data: { to: string[]; cc?: string[]; bcc?: string[]; subject: string; body: string }) => {
|
||||
const newEmail = {
|
||||
id: String(Date.now()),
|
||||
testId: String(Date.now()),
|
||||
from: 'You',
|
||||
to: data.to,
|
||||
subject: data.subject,
|
||||
@@ -180,81 +175,40 @@ export default function EmailClientContent() {
|
||||
setSelectedEmailId(null)
|
||||
}, [])
|
||||
|
||||
// ── Sidebar ──────────────────────────────────────────────────────────────
|
||||
const sidebar = (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Box sx={{ padding: '16px' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => setShowCompose(true)}
|
||||
sx={{ width: '100%', borderRadius: '24px', padding: '12px 24px', fontSize: '0.9375rem', fontWeight: 500, textTransform: 'none', boxShadow: '0 1px 3px rgba(0,0,0,0.12)' }}
|
||||
>
|
||||
✏️ Compose
|
||||
</Button>
|
||||
</Box>
|
||||
<FolderNavigation items={folders} onNavigate={handleNavigateFolder} />
|
||||
</Box>
|
||||
)
|
||||
const unreadCount = filteredEmails.filter(e => !e.isRead).length
|
||||
|
||||
// ── Header ───────────────────────────────────────────────────────────────
|
||||
const header = (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '12px', width: '100%', padding: '0 8px' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontSize: '24px' }}>📧</span>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: '1.125rem' }}>
|
||||
MetaMail
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, maxWidth: '680px', margin: '0 auto' }}>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search mail"
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 16px',
|
||||
borderRadius: '24px',
|
||||
border: 'none',
|
||||
backgroundColor: '#f1f3f4',
|
||||
fontSize: '0.9375rem',
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<IconButton aria-label="Settings" title="Settings">
|
||||
<span style={{ fontSize: '20px' }}>⚙️</span>
|
||||
</IconButton>
|
||||
<Box sx={{
|
||||
width: '32px', height: '32px', borderRadius: '50%',
|
||||
backgroundColor: '#1976d2', color: 'white',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: '0.875rem', fontWeight: 600, cursor: 'pointer',
|
||||
}}>
|
||||
U
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<MailboxHeader
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
onSettingsClick={() => {}}
|
||||
/>
|
||||
)
|
||||
|
||||
const sidebar = (
|
||||
<MailboxSidebar
|
||||
folders={folders}
|
||||
onNavigate={handleNavigateFolder}
|
||||
onCompose={() => setShowCompose(true)}
|
||||
/>
|
||||
)
|
||||
|
||||
// ── Main (thread list) ───────────────────────────────────────────────────
|
||||
const main = (
|
||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ padding: '8px 16px', borderBottom: '1px solid #e0e0e0', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2" sx={{ color: '#5f6368', fontWeight: 500, textTransform: 'capitalize' }}>
|
||||
<Box className="mailbox-thread-panel">
|
||||
<Box className="mailbox-thread-toolbar">
|
||||
<Typography variant="body2" className="mailbox-thread-folder-label">
|
||||
{activeFolder} {filteredEmails.length > 0 && `(${filteredEmails.length})`}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#5f6368' }}>
|
||||
{filteredEmails.filter(e => !e.isRead).length} unread
|
||||
<Typography variant="caption" className="mailbox-thread-unread-label">
|
||||
{unreadCount} unread
|
||||
</Typography>
|
||||
</Box>
|
||||
{filteredEmails.length === 0 ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', flex: 1, color: '#5f6368', gap: '8px' }}>
|
||||
<span style={{ fontSize: '48px', opacity: 0.5 }}>
|
||||
<Box className="mailbox-empty-state">
|
||||
<span className="mailbox-empty-icon">
|
||||
{activeFolder === 'starred' ? '⭐' : activeFolder === 'trash' ? '🗑️' : '📭'}
|
||||
</span>
|
||||
<Typography variant="body1">
|
||||
<Typography variant="body2">
|
||||
{activeFolder === 'inbox' && searchQuery ? 'No results found' : `No messages in ${activeFolder}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -270,53 +224,16 @@ export default function EmailClientContent() {
|
||||
</Box>
|
||||
)
|
||||
|
||||
// ── Detail (selected email) ──────────────────────────────────────────────
|
||||
const detail = selectedEmail ? (
|
||||
<Box sx={{ height: '100%', overflow: 'auto', padding: '16px' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '16px' }}>
|
||||
<button
|
||||
onClick={() => setSelectedEmailId(null)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '1.25rem', padding: '4px' }}
|
||||
aria-label="Back to list"
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<Box sx={{ display: 'flex', gap: '4px' }}>
|
||||
<IconButton aria-label="Archive" title="Archive">
|
||||
<span style={{ fontSize: '18px' }}>📥</span>
|
||||
</IconButton>
|
||||
<IconButton aria-label="Delete" title="Delete">
|
||||
<span style={{ fontSize: '18px' }}>🗑️</span>
|
||||
</IconButton>
|
||||
<IconButton aria-label="Reply" title="Reply">
|
||||
<span style={{ fontSize: '18px' }}>↩️</span>
|
||||
</IconButton>
|
||||
<IconButton aria-label="Forward" title="Forward">
|
||||
<span style={{ fontSize: '18px' }}>↪️</span>
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
<EmailHeader
|
||||
from={selectedEmail.from}
|
||||
to={selectedEmail.to || ['you@metabuilder.io']}
|
||||
cc={selectedEmail.cc}
|
||||
subject={selectedEmail.subject}
|
||||
receivedAt={selectedEmail.receivedAt}
|
||||
isStarred={selectedEmail.isStarred}
|
||||
onToggleStar={(starred) => handleToggleStar(selectedEmail.id, starred)}
|
||||
/>
|
||||
<Box sx={{ marginTop: '24px', fontSize: '0.9375rem', lineHeight: 1.7, whiteSpace: 'pre-wrap', color: '#3c4043' }}>
|
||||
{selectedEmail.body}
|
||||
</Box>
|
||||
<Box sx={{ marginTop: '32px', borderTop: '1px solid #e0e0e0', paddingTop: '16px' }}>
|
||||
<Button variant="outlined" onClick={() => setShowCompose(true)} sx={{ borderRadius: '20px', textTransform: 'none' }}>
|
||||
↩️ Reply
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={() => setShowCompose(true)} sx={{ borderRadius: '20px', textTransform: 'none', marginLeft: '8px' }}>
|
||||
↪️ Forward
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<EmailDetail
|
||||
email={selectedEmail}
|
||||
onClose={() => setSelectedEmailId(null)}
|
||||
onArchive={() => {}}
|
||||
onDelete={() => {}}
|
||||
onReply={() => setShowCompose(true)}
|
||||
onForward={() => setShowCompose(true)}
|
||||
onToggleStar={(starred) => handleToggleStar(selectedEmail.id, starred)}
|
||||
/>
|
||||
) : undefined
|
||||
|
||||
return (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user