mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
refactor(fakemui): Migrate email components from email-wip to email directory
- Move 22 email components from email-wip/ to email/ (production ready) - Components organized by category: atoms (3), inputs (3), surfaces (4), data-display (4), feedback (2), layout (3), navigation (2) - Total 29 files migrated including category index files - Update index files for data-display, inputs, surfaces categories - Restore I18nNavigation.ts and hooks.ts (actively used by postgres module) - email-wip directory removed as part of migration Files migrated: - Atoms: AttachmentIcon, MarkAsReadCheckbox, StarButton - Inputs: BodyEditor, EmailAddressInput, RecipientInput - Surfaces: ComposeWindow, EmailCard, MessageThread, SignatureCard - Data-Display: AttachmentList, EmailHeader, FolderTree, ThreadList - Feedback: SyncProgress, SyncStatusBadge - Layout: ComposerLayout, MailboxLayout, SettingsLayout - Navigation: AccountTabs, FolderNavigation Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
12
fakemui/react/components/data-display/index.ts
Normal file
12
fakemui/react/components/data-display/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export { Avatar, AvatarGroup } from './Avatar'
|
||||
export { Badge } from './Badge'
|
||||
export { Chip } from './Chip'
|
||||
export { Divider } from './Divider'
|
||||
export { Icon } from './Icon'
|
||||
export { List, ListItem, ListItemButton, ListItemIcon, ListItemText, ListItemAvatar, ListSubheader } from './List'
|
||||
export { Table, TableHead, TableBody, TableFooter, TableRow, TableCell, TableContainer, TablePagination, TableSortLabel } from './Table'
|
||||
export { Tooltip } from './Tooltip'
|
||||
export { TreeView } from './TreeView'
|
||||
export { Typography } from './Typography'
|
||||
export { Markdown } from './Markdown'
|
||||
export { Separator } from './Separator'
|
||||
47
fakemui/react/components/email/atoms/AttachmentIcon.tsx
Normal file
47
fakemui/react/components/email/atoms/AttachmentIcon.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React, { forwardRef } from 'react'
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
|
||||
export interface AttachmentIconProps extends React.SVGAttributes<SVGSVGElement> {
|
||||
filename?: string
|
||||
mimeType?: string
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const AttachmentIcon = forwardRef<SVGSVGElement, AttachmentIconProps>(
|
||||
({ filename, mimeType, testId: customTestId, ...props }, ref) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'attachment-icon',
|
||||
identifier: customTestId || filename || 'attachment'
|
||||
})
|
||||
|
||||
// Determine icon based on mime type
|
||||
const getIconContent = () => {
|
||||
if (mimeType?.startsWith('image/')) return '🖼️'
|
||||
if (mimeType?.startsWith('video/')) return '🎬'
|
||||
if (mimeType?.startsWith('audio/')) return '🎵'
|
||||
if (mimeType === 'application/pdf') return '📄'
|
||||
return '📎'
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
height="20"
|
||||
className="attachment-icon"
|
||||
role="img"
|
||||
aria-label={`Attachment: ${filename || 'document'}`}
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
<text x="50%" y="50%" dominantBaseline="middle" textAnchor="middle" fontSize="16">
|
||||
{getIconContent()}
|
||||
</text>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
AttachmentIcon.displayName = 'AttachmentIcon'
|
||||
42
fakemui/react/components/email/atoms/MarkAsReadCheckbox.tsx
Normal file
42
fakemui/react/components/email/atoms/MarkAsReadCheckbox.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { forwardRef, useState } from 'react'
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
|
||||
export interface MarkAsReadCheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
isRead?: boolean
|
||||
onToggleRead?: (read: boolean) => void
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const MarkAsReadCheckbox = forwardRef<HTMLInputElement, MarkAsReadCheckboxProps>(
|
||||
({ isRead = false, onToggleRead, testId: customTestId, ...props }, ref) => {
|
||||
const [read, setRead] = useState(isRead)
|
||||
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'read-checkbox',
|
||||
identifier: customTestId || 'read-status'
|
||||
})
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newState = e.target.checked
|
||||
setRead(newState)
|
||||
onToggleRead?.(newState)
|
||||
props.onChange?.(e)
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
checked={read}
|
||||
className="read-checkbox"
|
||||
aria-label="Mark as read"
|
||||
{...accessible}
|
||||
{...props}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
MarkAsReadCheckbox.displayName = 'MarkAsReadCheckbox'
|
||||
43
fakemui/react/components/email/atoms/StarButton.tsx
Normal file
43
fakemui/react/components/email/atoms/StarButton.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, { forwardRef, useState } from 'react'
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
|
||||
export interface StarButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
isStarred?: boolean
|
||||
onToggleStar?: (starred: boolean) => void
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const StarButton = forwardRef<HTMLButtonElement, StarButtonProps>(
|
||||
({ isStarred = false, onToggleStar, testId: customTestId, ...props }, ref) => {
|
||||
const [starred, setStarred] = useState(isStarred)
|
||||
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'star-button',
|
||||
identifier: customTestId || 'star'
|
||||
})
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const newState = !starred
|
||||
setStarred(newState)
|
||||
onToggleStar?.(newState)
|
||||
props.onClick?.(e)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={`star-button ${starred ? 'star-button--active' : ''}`}
|
||||
aria-pressed={starred}
|
||||
title={starred ? 'Remove star' : 'Add star'}
|
||||
{...accessible}
|
||||
{...props}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{starred ? '⭐' : '☆'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
StarButton.displayName = 'StarButton'
|
||||
3
fakemui/react/components/email/atoms/index.ts
Normal file
3
fakemui/react/components/email/atoms/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { AttachmentIcon, type AttachmentIconProps } from './AttachmentIcon'
|
||||
export { StarButton, type StarButtonProps } from './StarButton'
|
||||
export { MarkAsReadCheckbox, type MarkAsReadCheckboxProps } from './MarkAsReadCheckbox'
|
||||
@@ -0,0 +1,87 @@
|
||||
// fakemui/react/components/email/data-display/AttachmentList.tsx
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Box, type BoxProps }
|
||||
import { Typography }
|
||||
import { Button }
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
import { AttachmentIcon }
|
||||
|
||||
export interface Attachment {
|
||||
id: string
|
||||
filename: string
|
||||
mimeType: string
|
||||
size: number
|
||||
downloadUrl?: string
|
||||
}
|
||||
|
||||
export interface AttachmentListProps extends BoxProps {
|
||||
attachments: Attachment[]
|
||||
onDownload?: (attachment: Attachment) => void
|
||||
onDelete?: (attachmentId: string) => void
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const AttachmentList = forwardRef<HTMLDivElement, AttachmentListProps>(
|
||||
({ attachments, onDownload, onDelete, testId: customTestId, ...props }, ref) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'attachment-list',
|
||||
identifier: customTestId || 'attachments'
|
||||
})
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className="attachment-list"
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
{attachments.length === 0 ? (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
No attachments
|
||||
</Typography>
|
||||
) : (
|
||||
<ul className="attachment-items">
|
||||
{attachments.map((attachment) => (
|
||||
<li key={attachment.id} className="attachment-item">
|
||||
<AttachmentIcon filename={attachment.filename} mimeType={attachment.mimeType} />
|
||||
<div className="attachment-info">
|
||||
<Typography variant="body2">{attachment.filename}</Typography>
|
||||
<Typography variant="caption">{formatFileSize(attachment.size)}</Typography>
|
||||
</div>
|
||||
<div className="attachment-actions">
|
||||
{attachment.downloadUrl && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onDownload?.(attachment)}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onDelete?.(attachment.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
AttachmentList.displayName = 'AttachmentList'
|
||||
76
fakemui/react/components/email/data-display/EmailHeader.tsx
Normal file
76
fakemui/react/components/email/data-display/EmailHeader.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
// fakemui/react/components/email/data-display/EmailHeader.tsx
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Box, BoxProps, Typography }
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
import { StarButton }
|
||||
|
||||
export interface EmailHeaderProps extends BoxProps {
|
||||
from: string
|
||||
to: string[]
|
||||
cc?: string[]
|
||||
subject: string
|
||||
receivedAt: number
|
||||
isStarred?: boolean
|
||||
onToggleStar?: (starred: boolean) => void
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const EmailHeader = forwardRef<HTMLDivElement, EmailHeaderProps>(
|
||||
(
|
||||
{
|
||||
from,
|
||||
to,
|
||||
cc,
|
||||
subject,
|
||||
receivedAt,
|
||||
isStarred = false,
|
||||
onToggleStar,
|
||||
testId: customTestId,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'email-header',
|
||||
identifier: customTestId || subject
|
||||
})
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className="email-header"
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
<div className="header-top">
|
||||
<Typography variant="h5" className="subject">
|
||||
{subject}
|
||||
</Typography>
|
||||
<StarButton
|
||||
isStarred={isStarred}
|
||||
onToggleStar={onToggleStar}
|
||||
/>
|
||||
</div>
|
||||
<div className="header-details">
|
||||
<Typography variant="body2" className="from">
|
||||
From: <strong>{from}</strong>
|
||||
</Typography>
|
||||
<Typography variant="body2" className="to">
|
||||
To: <strong>{to.join(', ')}</strong>
|
||||
</Typography>
|
||||
{cc && cc.length > 0 && (
|
||||
<Typography variant="body2" className="cc">
|
||||
Cc: <strong>{cc.join(', ')}</strong>
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="caption" className="date">
|
||||
{new Date(receivedAt).toLocaleString()}
|
||||
</Typography>
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
EmailHeader.displayName = 'EmailHeader'
|
||||
84
fakemui/react/components/email/data-display/FolderTree.tsx
Normal file
84
fakemui/react/components/email/data-display/FolderTree.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
// fakemui/react/components/email/data-display/FolderTree.tsx
|
||||
import React, { forwardRef, useState } from 'react'
|
||||
import { Box, BoxProps, Typography }
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
|
||||
export interface FolderNode {
|
||||
id: string
|
||||
name: string
|
||||
unreadCount?: number
|
||||
children?: FolderNode[]
|
||||
type?: 'inbox' | 'sent' | 'drafts' | 'trash' | 'custom'
|
||||
}
|
||||
|
||||
export interface FolderTreeProps extends BoxProps {
|
||||
folders: FolderNode[]
|
||||
selectedFolderId?: string
|
||||
onSelectFolder?: (folderId: string) => void
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const FolderTree = forwardRef<HTMLDivElement, FolderTreeProps>(
|
||||
({ folders, selectedFolderId, onSelectFolder, testId: customTestId, ...props }, ref) => {
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set())
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'folder-tree',
|
||||
identifier: customTestId || 'folders'
|
||||
})
|
||||
|
||||
const toggleFolder = (folderId: string) => {
|
||||
const newExpanded = new Set(expandedFolders)
|
||||
if (newExpanded.has(folderId)) {
|
||||
newExpanded.delete(folderId)
|
||||
} else {
|
||||
newExpanded.add(folderId)
|
||||
}
|
||||
setExpandedFolders(newExpanded)
|
||||
}
|
||||
|
||||
const renderFolder = (folder: FolderNode, level: number = 0) => (
|
||||
<div key={folder.id} className="folder-item" style={{ paddingLeft: `${level * 16}px` }}>
|
||||
<button
|
||||
className={`folder-btn ${selectedFolderId === folder.id ? 'folder-btn--active' : ''}`}
|
||||
onClick={() => onSelectFolder?.(folder.id)}
|
||||
>
|
||||
{folder.children && folder.children.length > 0 && (
|
||||
<span
|
||||
className="folder-expand"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleFolder(folder.id)
|
||||
}}
|
||||
>
|
||||
{expandedFolders.has(folder.id) ? '▼' : '▶'}
|
||||
</span>
|
||||
)}
|
||||
<span className="folder-icon">📁</span>
|
||||
<Typography variant="body2">{folder.name}</Typography>
|
||||
{folder.unreadCount ? (
|
||||
<span className="unread-badge">{folder.unreadCount}</span>
|
||||
) : null}
|
||||
</button>
|
||||
{expandedFolders.has(folder.id) && folder.children && (
|
||||
<div className="folder-children">
|
||||
{folder.children.map((child) => renderFolder(child, level + 1))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className="folder-tree"
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
{folders.map((folder) => renderFolder(folder))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
FolderTree.displayName = 'FolderTree'
|
||||
60
fakemui/react/components/email/data-display/ThreadList.tsx
Normal file
60
fakemui/react/components/email/data-display/ThreadList.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
// fakemui/react/components/email/data-display/ThreadList.tsx
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Box, BoxProps }
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
import { EmailCard, type EmailCardProps }
|
||||
|
||||
export interface ThreadListProps extends BoxProps {
|
||||
emails: Array<Omit<EmailCardProps, 'onSelect' | 'onToggleRead' | 'onToggleStar' | 'ref'>>
|
||||
selectedEmailId?: string
|
||||
onSelectEmail?: (emailId: string) => void
|
||||
onToggleRead?: (emailId: string, read: boolean) => void
|
||||
onToggleStar?: (emailId: string, starred: boolean) => void
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const ThreadList = forwardRef<HTMLDivElement, ThreadListProps>(
|
||||
(
|
||||
{
|
||||
emails,
|
||||
selectedEmailId,
|
||||
onSelectEmail,
|
||||
onToggleRead,
|
||||
onToggleStar,
|
||||
testId: customTestId,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'thread-list',
|
||||
identifier: customTestId || 'threads'
|
||||
})
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className="thread-list"
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
{emails.length === 0 ? (
|
||||
<div className="no-emails">No emails</div>
|
||||
) : (
|
||||
emails.map((email, idx) => (
|
||||
<EmailCard
|
||||
key={idx}
|
||||
{...email}
|
||||
onSelect={() => onSelectEmail?.(email.testId || `email-${idx}`)}
|
||||
onToggleRead={(read) => onToggleRead?.(email.testId || `email-${idx}`, read)}
|
||||
onToggleStar={(starred) => onToggleStar?.(email.testId || `email-${idx}`, starred)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ThreadList.displayName = 'ThreadList'
|
||||
4
fakemui/react/components/email/data-display/index.ts
Normal file
4
fakemui/react/components/email/data-display/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { AttachmentList, type AttachmentListProps, type Attachment } from './AttachmentList'
|
||||
export { EmailHeader, type EmailHeaderProps } from './EmailHeader'
|
||||
export { FolderTree, type FolderTreeProps, type FolderNode } from './FolderTree'
|
||||
export { ThreadList, type ThreadListProps } from './ThreadList'
|
||||
56
fakemui/react/components/email/feedback/SyncProgress.tsx
Normal file
56
fakemui/react/components/email/feedback/SyncProgress.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
// fakemui/react/components/email/feedback/SyncProgress.tsx
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Box, type BoxProps }
|
||||
import { LinearProgress, Typography }
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
|
||||
export interface SyncProgressProps extends BoxProps {
|
||||
progress: number
|
||||
totalMessages?: number
|
||||
syncedMessages?: number
|
||||
isVisible?: boolean
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const SyncProgress = forwardRef<HTMLDivElement, SyncProgressProps>(
|
||||
(
|
||||
{
|
||||
progress,
|
||||
totalMessages = 0,
|
||||
syncedMessages = 0,
|
||||
isVisible = true,
|
||||
testId: customTestId,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'sync-progress',
|
||||
identifier: customTestId || 'progress'
|
||||
})
|
||||
|
||||
if (!isVisible) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className="sync-progress"
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
Syncing... {syncedMessages} of {totalMessages} messages
|
||||
</Typography>
|
||||
<LinearProgress variant="determinate" value={progress} />
|
||||
<Typography variant="caption">
|
||||
{Math.round(progress)}% complete
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SyncProgress.displayName = 'SyncProgress'
|
||||
69
fakemui/react/components/email/feedback/SyncStatusBadge.tsx
Normal file
69
fakemui/react/components/email/feedback/SyncStatusBadge.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
// fakemui/react/components/email/feedback/SyncStatusBadge.tsx
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Box, type BoxProps }
|
||||
import { Chip }
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
|
||||
export type SyncStatus = 'syncing' | 'synced' | 'error' | 'idle'
|
||||
|
||||
export interface SyncStatusBadgeProps extends BoxProps {
|
||||
status: SyncStatus
|
||||
lastSyncAt?: number
|
||||
errorMessage?: string
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const SyncStatusBadge = forwardRef<HTMLDivElement, SyncStatusBadgeProps>(
|
||||
({ status, lastSyncAt, errorMessage, testId: customTestId, ...props }, ref) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'sync-badge',
|
||||
identifier: customTestId || status
|
||||
})
|
||||
|
||||
const getStatusLabel = () => {
|
||||
switch (status) {
|
||||
case 'syncing':
|
||||
return 'Syncing...'
|
||||
case 'synced':
|
||||
return `Last sync: ${lastSyncAt ? new Date(lastSyncAt).toLocaleTimeString() : 'now'}`
|
||||
case 'error':
|
||||
return `Sync failed: ${errorMessage || 'Unknown error'}`
|
||||
case 'idle':
|
||||
return 'Idle'
|
||||
default:
|
||||
return 'Unknown'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (status) {
|
||||
case 'syncing':
|
||||
return 'info'
|
||||
case 'synced':
|
||||
return 'success'
|
||||
case 'error':
|
||||
return 'error'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className="sync-status-badge"
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
<Chip
|
||||
label={getStatusLabel()}
|
||||
color={getStatusColor()}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SyncStatusBadge.displayName = 'SyncStatusBadge'
|
||||
2
fakemui/react/components/email/feedback/index.ts
Normal file
2
fakemui/react/components/email/feedback/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { SyncStatusBadge, type SyncStatusBadgeProps, type SyncStatus } from './SyncStatusBadge'
|
||||
export { SyncProgress, type SyncProgressProps } from './SyncProgress'
|
||||
75
fakemui/react/components/email/index.ts
Normal file
75
fakemui/react/components/email/index.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
// Atoms
|
||||
export {
|
||||
AttachmentIcon,
|
||||
type AttachmentIconProps,
|
||||
StarButton,
|
||||
type StarButtonProps,
|
||||
MarkAsReadCheckbox,
|
||||
type MarkAsReadCheckboxProps,
|
||||
} from './atoms'
|
||||
|
||||
// Inputs
|
||||
export {
|
||||
EmailAddressInput,
|
||||
type EmailAddressInputProps,
|
||||
RecipientInput,
|
||||
type RecipientInputProps,
|
||||
BodyEditor,
|
||||
type BodyEditorProps,
|
||||
} from './inputs'
|
||||
|
||||
// Surfaces
|
||||
export {
|
||||
EmailCard,
|
||||
type EmailCardProps,
|
||||
MessageThread,
|
||||
type MessageThreadProps,
|
||||
ComposeWindow,
|
||||
type ComposeWindowProps,
|
||||
SignatureCard,
|
||||
type SignatureCardProps,
|
||||
} from './surfaces'
|
||||
|
||||
// Data Display
|
||||
export {
|
||||
AttachmentList,
|
||||
type AttachmentListProps,
|
||||
type Attachment,
|
||||
EmailHeader,
|
||||
type EmailHeaderProps,
|
||||
FolderTree,
|
||||
type FolderTreeProps,
|
||||
type FolderNode,
|
||||
ThreadList,
|
||||
type ThreadListProps,
|
||||
} from './data-display'
|
||||
|
||||
// Feedback
|
||||
export {
|
||||
SyncStatusBadge,
|
||||
type SyncStatusBadgeProps,
|
||||
type SyncStatus,
|
||||
SyncProgress,
|
||||
type SyncProgressProps,
|
||||
} from './feedback'
|
||||
|
||||
// Layout
|
||||
export {
|
||||
MailboxLayout,
|
||||
type MailboxLayoutProps,
|
||||
ComposerLayout,
|
||||
type ComposerLayoutProps,
|
||||
SettingsLayout,
|
||||
type SettingsLayoutProps,
|
||||
type SettingsSection,
|
||||
} from './layout'
|
||||
|
||||
// Navigation
|
||||
export {
|
||||
AccountTabs,
|
||||
type AccountTabsProps,
|
||||
type EmailAccount,
|
||||
FolderNavigation,
|
||||
type FolderNavigationProps,
|
||||
type FolderNavigationItem,
|
||||
} from './navigation'
|
||||
50
fakemui/react/components/email/inputs/BodyEditor.tsx
Normal file
50
fakemui/react/components/email/inputs/BodyEditor.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
// fakemui/react/components/email/inputs/BodyEditor.tsx
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Box } from '../../layout/Box'
|
||||
import { useAccessible } from '../../../../src/utils/useAccessible'
|
||||
|
||||
export interface BodyEditorProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
mode?: 'plain' | 'html'
|
||||
onModeChange?: (mode: 'plain' | 'html') => void
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const BodyEditor = forwardRef<HTMLTextAreaElement, BodyEditorProps>(
|
||||
({ mode = 'plain', onModeChange, testId: customTestId, ...props }, ref) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'body-editor',
|
||||
identifier: customTestId || 'body'
|
||||
})
|
||||
|
||||
return (
|
||||
<Box className="body-editor">
|
||||
<div className="body-editor-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
className={`mode-btn ${mode === 'plain' ? 'mode-btn--active' : ''}`}
|
||||
onClick={() => onModeChange?.('plain')}
|
||||
>
|
||||
Plain Text
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`mode-btn ${mode === 'html' ? 'mode-btn--active' : ''}`}
|
||||
onClick={() => onModeChange?.('html')}
|
||||
>
|
||||
HTML
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
ref={ref}
|
||||
className="body-editor-textarea"
|
||||
placeholder="Write your message here..."
|
||||
{...accessible}
|
||||
{...props}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BodyEditor.displayName = 'BodyEditor'
|
||||
48
fakemui/react/components/email/inputs/EmailAddressInput.tsx
Normal file
48
fakemui/react/components/email/inputs/EmailAddressInput.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
// fakemui/react/components/email/inputs/EmailAddressInput.tsx
|
||||
import React, { forwardRef } from 'react'
|
||||
import { TextField, TextFieldProps } from '../../inputs/TextField'
|
||||
import { useAccessible } from '../../../../src/utils/useAccessible'
|
||||
|
||||
export interface EmailAddressInputProps extends Omit<TextFieldProps, 'type'> {
|
||||
onValidate?: (valid: boolean) => void
|
||||
allowMultiple?: boolean
|
||||
}
|
||||
|
||||
export const EmailAddressInput = forwardRef<HTMLInputElement, EmailAddressInputProps>(
|
||||
({ onValidate, allowMultiple = false, testId: customTestId, ...props }, ref) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'email-input',
|
||||
identifier: customTestId || 'email'
|
||||
})
|
||||
|
||||
const validateEmail = (value: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (allowMultiple) {
|
||||
const emails = value.split(',').map(e => e.trim())
|
||||
return emails.every(e => emailRegex.test(e) || e === '')
|
||||
}
|
||||
return emailRegex.test(value) || value === ''
|
||||
}
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const valid = validateEmail(e.target.value)
|
||||
onValidate?.(valid)
|
||||
props.onChange?.(e)
|
||||
}
|
||||
|
||||
return (
|
||||
<TextField
|
||||
ref={ref}
|
||||
type="email"
|
||||
label={props.label || (allowMultiple ? 'Recipients' : 'Email Address')}
|
||||
placeholder={allowMultiple ? 'user@example.com, another@example.com' : 'user@example.com'}
|
||||
{...accessible}
|
||||
{...props}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
EmailAddressInput.displayName = 'EmailAddressInput'
|
||||
67
fakemui/react/components/email/inputs/RecipientInput.tsx
Normal file
67
fakemui/react/components/email/inputs/RecipientInput.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
// fakemui/react/components/email/inputs/RecipientInput.tsx
|
||||
import React, { forwardRef, useState } from 'react'
|
||||
import { Box } from '../../layout/Box'
|
||||
import { TextField } from '../../inputs/TextField'
|
||||
import { Chip } from '../../data-display/Chip'
|
||||
import { useAccessible } from '../../../../src/utils/useAccessible'
|
||||
|
||||
export interface RecipientInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
recipients?: string[]
|
||||
onRecipientsChange?: (recipients: string[]) => void
|
||||
recipientType?: 'to' | 'cc' | 'bcc'
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const RecipientInput = forwardRef<HTMLInputElement, RecipientInputProps>(
|
||||
({ recipients = [], onRecipientsChange, recipientType = 'to', testId: customTestId, ...props }, ref) => {
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'recipient-input',
|
||||
identifier: customTestId || recipientType
|
||||
})
|
||||
|
||||
const handleAddRecipient = () => {
|
||||
if (inputValue && inputValue.includes('@')) {
|
||||
const newRecipients = [...recipients, inputValue.trim()]
|
||||
onRecipientsChange?.(newRecipients)
|
||||
setInputValue('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveRecipient = (index: number) => {
|
||||
const newRecipients = recipients.filter((_, i) => i !== index)
|
||||
onRecipientsChange?.(newRecipients)
|
||||
}
|
||||
|
||||
// Filter out incompatible HTML input attributes
|
||||
const { size: _size, ...textFieldProps } = props
|
||||
|
||||
return (
|
||||
<Box className="recipient-input">
|
||||
<div className="recipient-chips">
|
||||
{recipients.map((recipient, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
onDelete={() => handleRemoveRecipient(index)}
|
||||
>
|
||||
{recipient}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
<TextField
|
||||
ref={ref}
|
||||
type="email"
|
||||
placeholder={`Add ${recipientType} recipient...`}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleAddRecipient()}
|
||||
{...accessible}
|
||||
{...textFieldProps}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
RecipientInput.displayName = 'RecipientInput'
|
||||
3
fakemui/react/components/email/inputs/index.ts
Normal file
3
fakemui/react/components/email/inputs/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { EmailAddressInput, type EmailAddressInputProps } from './EmailAddressInput'
|
||||
export { RecipientInput, type RecipientInputProps } from './RecipientInput'
|
||||
export { BodyEditor, type BodyEditorProps } from './BodyEditor'
|
||||
34
fakemui/react/components/email/layout/ComposerLayout.tsx
Normal file
34
fakemui/react/components/email/layout/ComposerLayout.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
// fakemui/react/components/email/layout/ComposerLayout.tsx
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Box, BoxProps }
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
|
||||
export interface ComposerLayoutProps extends BoxProps {
|
||||
form: React.ReactNode
|
||||
preview?: React.ReactNode
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const ComposerLayout = forwardRef<HTMLDivElement, ComposerLayoutProps>(
|
||||
({ form, preview, testId: customTestId, ...props }, ref) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'composer-layout',
|
||||
identifier: customTestId || 'composer'
|
||||
})
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className="composer-layout"
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
<Box className="composer-form">{form}</Box>
|
||||
{preview && <Box className="composer-preview">{preview}</Box>}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ComposerLayout.displayName = 'ComposerLayout'
|
||||
45
fakemui/react/components/email/layout/MailboxLayout.tsx
Normal file
45
fakemui/react/components/email/layout/MailboxLayout.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
// fakemui/react/components/email/layout/MailboxLayout.tsx
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Box, type BoxProps }
|
||||
import { AppBar, Toolbar }
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
|
||||
export interface MailboxLayoutProps extends BoxProps {
|
||||
sidebar: React.ReactNode
|
||||
main: React.ReactNode
|
||||
detail?: React.ReactNode
|
||||
header?: React.ReactNode
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const MailboxLayout = forwardRef<HTMLDivElement, MailboxLayoutProps>(
|
||||
({ sidebar, main, detail, header, testId: customTestId, ...props }, ref) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'mailbox-layout',
|
||||
identifier: customTestId || 'mailbox'
|
||||
})
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className="mailbox-layout"
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
{header && (
|
||||
<AppBar position="static" className="mailbox-header">
|
||||
<Toolbar>{header}</Toolbar>
|
||||
</AppBar>
|
||||
)}
|
||||
<Box className="mailbox-content">
|
||||
<aside className="mailbox-sidebar">{sidebar}</aside>
|
||||
<main className="mailbox-main">{main}</main>
|
||||
{detail && <article className="mailbox-detail">{detail}</article>}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
MailboxLayout.displayName = 'MailboxLayout'
|
||||
49
fakemui/react/components/email/layout/SettingsLayout.tsx
Normal file
49
fakemui/react/components/email/layout/SettingsLayout.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
// fakemui/react/components/email/layout/SettingsLayout.tsx
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Box, BoxProps, Tabs, Tab }
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
|
||||
export interface SettingsSection {
|
||||
id: string
|
||||
label: string
|
||||
content: React.ReactNode
|
||||
}
|
||||
|
||||
export interface SettingsLayoutProps extends BoxProps {
|
||||
sections: SettingsSection[]
|
||||
activeSection?: string
|
||||
onSectionChange?: (sectionId: string) => void
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const SettingsLayout = forwardRef<HTMLDivElement, SettingsLayoutProps>(
|
||||
({ sections, activeSection, onSectionChange, testId: customTestId, ...props }, ref) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'settings-layout',
|
||||
identifier: customTestId || 'settings'
|
||||
})
|
||||
|
||||
const current = activeSection || sections[0]?.id
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className="settings-layout"
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
<Tabs value={current} onChange={(_, value) => onSectionChange?.(value as string)}>
|
||||
{sections.map((section) => (
|
||||
<Tab key={section.id} label={section.label} value={section.id} />
|
||||
))}
|
||||
</Tabs>
|
||||
<Box className="settings-content">
|
||||
{sections.find((s) => s.id === current)?.content}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SettingsLayout.displayName = 'SettingsLayout'
|
||||
3
fakemui/react/components/email/layout/index.ts
Normal file
3
fakemui/react/components/email/layout/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { MailboxLayout, type MailboxLayoutProps } from './MailboxLayout'
|
||||
export { ComposerLayout, type ComposerLayoutProps } from './ComposerLayout'
|
||||
export { SettingsLayout, type SettingsLayoutProps, type SettingsSection } from './SettingsLayout'
|
||||
54
fakemui/react/components/email/navigation/AccountTabs.tsx
Normal file
54
fakemui/react/components/email/navigation/AccountTabs.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Tabs, Tab, TabsProps }
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
|
||||
export interface EmailAccount {
|
||||
id: string
|
||||
accountName: string
|
||||
emailAddress: string
|
||||
unreadCount?: number
|
||||
}
|
||||
|
||||
export interface AccountTabsProps extends Omit<TabsProps, 'onChange'> {
|
||||
accounts: EmailAccount[]
|
||||
selectedAccountId?: string
|
||||
onSelectAccount?: (accountId: string) => void
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const AccountTabs = forwardRef<HTMLDivElement, AccountTabsProps>(
|
||||
({ accounts, selectedAccountId, onSelectAccount, testId: customTestId, ...props }, ref) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'account-tabs',
|
||||
identifier: customTestId || 'accounts'
|
||||
})
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
ref={ref}
|
||||
value={selectedAccountId || (accounts[0]?.id ?? '')}
|
||||
onChange={(_, value) => onSelectAccount?.(value as string)}
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
{accounts.map((account) => (
|
||||
<Tab
|
||||
key={account.id}
|
||||
label={
|
||||
<span className="account-tab">
|
||||
<span className="account-name">{account.accountName}</span>
|
||||
{account.unreadCount ? (
|
||||
<span className="unread-badge">{account.unreadCount}</span>
|
||||
) : null}
|
||||
</span>
|
||||
}
|
||||
value={account.id}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
AccountTabs.displayName = 'AccountTabs'
|
||||
@@ -0,0 +1,56 @@
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Box, BoxProps, Button }
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
|
||||
export interface FolderNavigationItem {
|
||||
id: string
|
||||
label: string
|
||||
icon?: string
|
||||
unreadCount?: number
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
export interface FolderNavigationProps extends BoxProps {
|
||||
items: FolderNavigationItem[]
|
||||
onNavigate?: (itemId: string) => void
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const FolderNavigation = forwardRef<HTMLDivElement, FolderNavigationProps>(
|
||||
({ items, onNavigate, testId: customTestId, ...props }, ref) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'folder-nav',
|
||||
identifier: customTestId || 'folders'
|
||||
})
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className="folder-navigation"
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
<nav className="folder-nav-list">
|
||||
{items.map((item) => (
|
||||
<Button
|
||||
key={item.id}
|
||||
variant={item.isActive ? 'primary' : 'ghost'}
|
||||
fullWidth
|
||||
className="folder-nav-item"
|
||||
onClick={() => onNavigate?.(item.id)}
|
||||
>
|
||||
{item.icon && <span className="folder-icon">{item.icon}</span>}
|
||||
<span className="folder-label">{item.label}</span>
|
||||
{item.unreadCount ? (
|
||||
<span className="unread-count">{item.unreadCount}</span>
|
||||
) : null}
|
||||
</Button>
|
||||
))}
|
||||
</nav>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
FolderNavigation.displayName = 'FolderNavigation'
|
||||
2
fakemui/react/components/email/navigation/index.ts
Normal file
2
fakemui/react/components/email/navigation/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { AccountTabs, type AccountTabsProps, type EmailAccount } from './AccountTabs'
|
||||
export { FolderNavigation, type FolderNavigationProps, type FolderNavigationItem } from './FolderNavigation'
|
||||
90
fakemui/react/components/email/surfaces/ComposeWindow.tsx
Normal file
90
fakemui/react/components/email/surfaces/ComposeWindow.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
// fakemui/react/components/email/surfaces/ComposeWindow.tsx
|
||||
import React, { forwardRef, useState } from 'react'
|
||||
import { Box, BoxProps, Button, Card }
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
import { EmailAddressInput, RecipientInput, BodyEditor }
|
||||
|
||||
export interface ComposeWindowProps extends BoxProps {
|
||||
onSend?: (data: { to: string[]; cc?: string[]; bcc?: string[]; subject: string; body: string }) => void
|
||||
onClose?: () => void
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const ComposeWindow = forwardRef<HTMLDivElement, ComposeWindowProps>(
|
||||
({ onSend, onClose, testId: customTestId, ...props }, ref) => {
|
||||
const [to, setTo] = useState<string[]>([])
|
||||
const [cc, setCc] = useState<string[]>([])
|
||||
const [bcc, setBcc] = useState<string[]>([])
|
||||
const [subject, setSubject] = useState('')
|
||||
const [body, setBody] = useState('')
|
||||
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'compose',
|
||||
identifier: customTestId || 'compose'
|
||||
})
|
||||
|
||||
const handleSend = () => {
|
||||
if (to.length > 0 && subject && body) {
|
||||
onSend?.({ to, cc, bcc, subject, body })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
ref={ref}
|
||||
className="compose-window"
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
<Box className="compose-header">
|
||||
<h2>Compose Email</h2>
|
||||
<button onClick={onClose} className="close-btn">
|
||||
×
|
||||
</button>
|
||||
</Box>
|
||||
<Box className="compose-body">
|
||||
<RecipientInput
|
||||
recipientType="to"
|
||||
recipients={to}
|
||||
onRecipientsChange={setTo}
|
||||
placeholder="To:"
|
||||
/>
|
||||
<RecipientInput
|
||||
recipientType="cc"
|
||||
recipients={cc}
|
||||
onRecipientsChange={setCc}
|
||||
placeholder="Cc:"
|
||||
/>
|
||||
<RecipientInput
|
||||
recipientType="bcc"
|
||||
recipients={bcc}
|
||||
onRecipientsChange={setBcc}
|
||||
placeholder="Bcc:"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Subject"
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
className="compose-subject"
|
||||
/>
|
||||
<BodyEditor
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="compose-footer">
|
||||
<Button variant="primary" onClick={handleSend}>
|
||||
Send
|
||||
</Button>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ComposeWindow.displayName = 'ComposeWindow'
|
||||
93
fakemui/react/components/email/surfaces/EmailCard.tsx
Normal file
93
fakemui/react/components/email/surfaces/EmailCard.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
// fakemui/react/components/email/surfaces/EmailCard.tsx
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Card }
|
||||
import { Box }
|
||||
import { Typography }
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
import { MarkAsReadCheckbox, StarButton }
|
||||
|
||||
export interface EmailCardProps extends CardProps {
|
||||
from: string
|
||||
subject: string
|
||||
preview: string
|
||||
receivedAt: number
|
||||
isRead: boolean
|
||||
isStarred?: boolean
|
||||
onSelect?: () => void
|
||||
onToggleRead?: (read: boolean) => void
|
||||
onToggleStar?: (starred: boolean) => void
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const EmailCard = forwardRef<HTMLDivElement, EmailCardProps>(
|
||||
(
|
||||
{
|
||||
from,
|
||||
subject,
|
||||
preview,
|
||||
receivedAt,
|
||||
isRead,
|
||||
isStarred = false,
|
||||
onSelect,
|
||||
onToggleRead,
|
||||
onToggleStar,
|
||||
testId: customTestId,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'card',
|
||||
identifier: customTestId || subject.substring(0, 20)
|
||||
})
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp)
|
||||
const today = new Date()
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
ref={ref}
|
||||
className={`email-card ${isRead ? 'email-card--read' : 'email-card--unread'}`}
|
||||
onClick={onSelect}
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
<Box className="email-card-header">
|
||||
<MarkAsReadCheckbox
|
||||
isRead={isRead}
|
||||
onToggleRead={onToggleRead}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<Typography variant="subtitle2" className="email-from">
|
||||
{from}
|
||||
</Typography>
|
||||
<div className="email-card-actions">
|
||||
<StarButton
|
||||
isStarred={isStarred}
|
||||
onToggleStar={onToggleStar}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<Typography variant="caption" className="email-date">
|
||||
{formatDate(receivedAt)}
|
||||
</Typography>
|
||||
</div>
|
||||
</Box>
|
||||
<Typography variant="h6" className="email-subject">
|
||||
{subject}
|
||||
</Typography>
|
||||
<Typography variant="body2" className="email-preview" noWrap>
|
||||
{preview}
|
||||
</Typography>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
EmailCard.displayName = 'EmailCard'
|
||||
54
fakemui/react/components/email/surfaces/MessageThread.tsx
Normal file
54
fakemui/react/components/email/surfaces/MessageThread.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
// fakemui/react/components/email/surfaces/MessageThread.tsx
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Box, BoxProps, Typography, Card }
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
|
||||
export interface MessageThreadProps extends BoxProps {
|
||||
messages: Array<{
|
||||
id: string
|
||||
from: string
|
||||
subject: string
|
||||
body: string
|
||||
receivedAt: number
|
||||
isStarred?: boolean
|
||||
}>
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const MessageThread = forwardRef<HTMLDivElement, MessageThreadProps>(
|
||||
({ messages, testId: customTestId, ...props }, ref) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'thread',
|
||||
identifier: customTestId || 'thread'
|
||||
})
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
className="message-thread"
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
{messages.map((message, index) => (
|
||||
<Card
|
||||
key={message.id}
|
||||
className={`message-item ${index === messages.length - 1 ? 'message-item--latest' : ''}`}
|
||||
>
|
||||
<Box className="message-header">
|
||||
<Typography variant="subtitle2">{message.from}</Typography>
|
||||
<Typography variant="caption">
|
||||
{new Date(message.receivedAt).toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" className="message-body">
|
||||
{message.body}
|
||||
</Typography>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
MessageThread.displayName = 'MessageThread'
|
||||
45
fakemui/react/components/email/surfaces/SignatureCard.tsx
Normal file
45
fakemui/react/components/email/surfaces/SignatureCard.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
// fakemui/react/components/email/surfaces/SignatureCard.tsx
|
||||
import React, { forwardRef } from 'react'
|
||||
import { Card, CardProps, Typography }
|
||||
import { useAccessible } from '@metabuilder/fakemui/hooks'
|
||||
|
||||
export interface SignatureCardProps extends CardProps {
|
||||
text: string
|
||||
editMode?: boolean
|
||||
onEdit?: (text: string) => void
|
||||
testId?: string
|
||||
}
|
||||
|
||||
export const SignatureCard = forwardRef<HTMLDivElement, SignatureCardProps>(
|
||||
({ text, editMode = false, onEdit, testId: customTestId, ...props }, ref) => {
|
||||
const accessible = useAccessible({
|
||||
feature: 'email',
|
||||
component: 'signature',
|
||||
identifier: customTestId || 'signature'
|
||||
})
|
||||
|
||||
return (
|
||||
<Card
|
||||
ref={ref}
|
||||
className="signature-card"
|
||||
{...accessible}
|
||||
{...props}
|
||||
>
|
||||
{editMode ? (
|
||||
<textarea
|
||||
value={text}
|
||||
onChange={(e) => onEdit?.(e.target.value)}
|
||||
className="signature-editor"
|
||||
placeholder="Add your signature here..."
|
||||
/>
|
||||
) : (
|
||||
<Typography variant="body2" className="signature-text">
|
||||
{text}
|
||||
</Typography>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SignatureCard.displayName = 'SignatureCard'
|
||||
5
fakemui/react/components/email/surfaces/index.ts
Normal file
5
fakemui/react/components/email/surfaces/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// fakemui/react/components/email/surfaces/index.ts
|
||||
export { EmailCard, type EmailCardProps } from './EmailCard'
|
||||
export { MessageThread, type MessageThreadProps } from './MessageThread'
|
||||
export { ComposeWindow, type ComposeWindowProps } from './ComposeWindow'
|
||||
export { SignatureCard, type SignatureCardProps } from './SignatureCard'
|
||||
27
fakemui/react/components/inputs/index.ts
Normal file
27
fakemui/react/components/inputs/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export { Button } from './Button'
|
||||
export { ButtonGroup } from './ButtonGroup'
|
||||
export { IconButton } from './IconButton'
|
||||
export { Fab } from './Fab'
|
||||
export { Input } from './Input'
|
||||
export { Textarea } from './Textarea'
|
||||
export { Select } from './Select'
|
||||
export { NativeSelect } from './NativeSelect'
|
||||
export { Checkbox } from './Checkbox'
|
||||
export { Radio } from './Radio'
|
||||
export { RadioGroup, useRadioGroup } from './RadioGroup'
|
||||
export { Switch } from './Switch'
|
||||
export { Slider } from './Slider'
|
||||
export { FormControl, useFormControl } from './FormControl'
|
||||
export { FormGroup } from './FormGroup'
|
||||
export { FormLabel } from './FormLabel'
|
||||
export { FormHelperText } from './FormHelperText'
|
||||
export { TextField } from './TextField'
|
||||
export { ToggleButton, ToggleButtonGroup } from './ToggleButton'
|
||||
export { Autocomplete } from './Autocomplete'
|
||||
export { Rating } from './Rating'
|
||||
export { ButtonBase, InputBase, FilledInput, OutlinedInput } from './InputBase'
|
||||
export { FormField } from './FormField'
|
||||
export { DatePicker } from './DatePicker'
|
||||
export { TimePicker } from './TimePicker'
|
||||
export { ColorPicker } from './ColorPicker'
|
||||
export { FileUpload } from './FileUpload'
|
||||
5
fakemui/react/components/surfaces/index.ts
Normal file
5
fakemui/react/components/surfaces/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { Paper } from './Paper'
|
||||
export { Card, CardHeader, CardContent, CardActions, CardActionArea, CardMedia, CardTitle, CardDescription, CardFooter } from './Card'
|
||||
export { Accordion, AccordionSummary, AccordionDetails, AccordionActions } from './Accordion'
|
||||
export { AppBar, Toolbar } from './AppBar'
|
||||
export { Drawer } from './Drawer'
|
||||
Reference in New Issue
Block a user