From c9fb2ce8e83efbe6f28aa8eeab6dfb92bfae294a Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Fri, 23 Jan 2026 20:33:36 +0000 Subject: [PATCH] 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 --- .../react/components/data-display/index.ts | 12 +++ .../components/email/atoms/AttachmentIcon.tsx | 47 ++++++++++ .../email/atoms/MarkAsReadCheckbox.tsx | 42 +++++++++ .../components/email/atoms/StarButton.tsx | 43 +++++++++ fakemui/react/components/email/atoms/index.ts | 3 + .../email/data-display/AttachmentList.tsx | 87 +++++++++++++++++ .../email/data-display/EmailHeader.tsx | 76 +++++++++++++++ .../email/data-display/FolderTree.tsx | 84 +++++++++++++++++ .../email/data-display/ThreadList.tsx | 60 ++++++++++++ .../components/email/data-display/index.ts | 4 + .../email/feedback/SyncProgress.tsx | 56 +++++++++++ .../email/feedback/SyncStatusBadge.tsx | 69 ++++++++++++++ .../react/components/email/feedback/index.ts | 2 + fakemui/react/components/email/index.ts | 75 +++++++++++++++ .../components/email/inputs/BodyEditor.tsx | 50 ++++++++++ .../email/inputs/EmailAddressInput.tsx | 48 ++++++++++ .../email/inputs/RecipientInput.tsx | 67 +++++++++++++ .../react/components/email/inputs/index.ts | 3 + .../email/layout/ComposerLayout.tsx | 34 +++++++ .../components/email/layout/MailboxLayout.tsx | 45 +++++++++ .../email/layout/SettingsLayout.tsx | 49 ++++++++++ .../react/components/email/layout/index.ts | 3 + .../email/navigation/AccountTabs.tsx | 54 +++++++++++ .../email/navigation/FolderNavigation.tsx | 56 +++++++++++ .../components/email/navigation/index.ts | 2 + .../email/surfaces/ComposeWindow.tsx | 90 ++++++++++++++++++ .../components/email/surfaces/EmailCard.tsx | 93 +++++++++++++++++++ .../email/surfaces/MessageThread.tsx | 54 +++++++++++ .../email/surfaces/SignatureCard.tsx | 45 +++++++++ .../react/components/email/surfaces/index.ts | 5 + fakemui/react/components/inputs/index.ts | 27 ++++++ fakemui/react/components/surfaces/index.ts | 5 + 32 files changed, 1390 insertions(+) create mode 100644 fakemui/react/components/data-display/index.ts create mode 100644 fakemui/react/components/email/atoms/AttachmentIcon.tsx create mode 100644 fakemui/react/components/email/atoms/MarkAsReadCheckbox.tsx create mode 100644 fakemui/react/components/email/atoms/StarButton.tsx create mode 100644 fakemui/react/components/email/atoms/index.ts create mode 100644 fakemui/react/components/email/data-display/AttachmentList.tsx create mode 100644 fakemui/react/components/email/data-display/EmailHeader.tsx create mode 100644 fakemui/react/components/email/data-display/FolderTree.tsx create mode 100644 fakemui/react/components/email/data-display/ThreadList.tsx create mode 100644 fakemui/react/components/email/data-display/index.ts create mode 100644 fakemui/react/components/email/feedback/SyncProgress.tsx create mode 100644 fakemui/react/components/email/feedback/SyncStatusBadge.tsx create mode 100644 fakemui/react/components/email/feedback/index.ts create mode 100644 fakemui/react/components/email/index.ts create mode 100644 fakemui/react/components/email/inputs/BodyEditor.tsx create mode 100644 fakemui/react/components/email/inputs/EmailAddressInput.tsx create mode 100644 fakemui/react/components/email/inputs/RecipientInput.tsx create mode 100644 fakemui/react/components/email/inputs/index.ts create mode 100644 fakemui/react/components/email/layout/ComposerLayout.tsx create mode 100644 fakemui/react/components/email/layout/MailboxLayout.tsx create mode 100644 fakemui/react/components/email/layout/SettingsLayout.tsx create mode 100644 fakemui/react/components/email/layout/index.ts create mode 100644 fakemui/react/components/email/navigation/AccountTabs.tsx create mode 100644 fakemui/react/components/email/navigation/FolderNavigation.tsx create mode 100644 fakemui/react/components/email/navigation/index.ts create mode 100644 fakemui/react/components/email/surfaces/ComposeWindow.tsx create mode 100644 fakemui/react/components/email/surfaces/EmailCard.tsx create mode 100644 fakemui/react/components/email/surfaces/MessageThread.tsx create mode 100644 fakemui/react/components/email/surfaces/SignatureCard.tsx create mode 100644 fakemui/react/components/email/surfaces/index.ts create mode 100644 fakemui/react/components/inputs/index.ts create mode 100644 fakemui/react/components/surfaces/index.ts diff --git a/fakemui/react/components/data-display/index.ts b/fakemui/react/components/data-display/index.ts new file mode 100644 index 000000000..f13efff5c --- /dev/null +++ b/fakemui/react/components/data-display/index.ts @@ -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' diff --git a/fakemui/react/components/email/atoms/AttachmentIcon.tsx b/fakemui/react/components/email/atoms/AttachmentIcon.tsx new file mode 100644 index 000000000..361d44eec --- /dev/null +++ b/fakemui/react/components/email/atoms/AttachmentIcon.tsx @@ -0,0 +1,47 @@ +import React, { forwardRef } from 'react' +import { useAccessible } from '@metabuilder/fakemui/hooks' + +export interface AttachmentIconProps extends React.SVGAttributes { + filename?: string + mimeType?: string + testId?: string +} + +export const AttachmentIcon = forwardRef( + ({ 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 ( + + + {getIconContent()} + + + ) + } +) + +AttachmentIcon.displayName = 'AttachmentIcon' diff --git a/fakemui/react/components/email/atoms/MarkAsReadCheckbox.tsx b/fakemui/react/components/email/atoms/MarkAsReadCheckbox.tsx new file mode 100644 index 000000000..5bd6b2085 --- /dev/null +++ b/fakemui/react/components/email/atoms/MarkAsReadCheckbox.tsx @@ -0,0 +1,42 @@ +import React, { forwardRef, useState } from 'react' +import { useAccessible } from '@metabuilder/fakemui/hooks' + +export interface MarkAsReadCheckboxProps extends React.InputHTMLAttributes { + isRead?: boolean + onToggleRead?: (read: boolean) => void + testId?: string +} + +export const MarkAsReadCheckbox = forwardRef( + ({ 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) => { + const newState = e.target.checked + setRead(newState) + onToggleRead?.(newState) + props.onChange?.(e) + } + + return ( + + ) + } +) + +MarkAsReadCheckbox.displayName = 'MarkAsReadCheckbox' diff --git a/fakemui/react/components/email/atoms/StarButton.tsx b/fakemui/react/components/email/atoms/StarButton.tsx new file mode 100644 index 000000000..285517536 --- /dev/null +++ b/fakemui/react/components/email/atoms/StarButton.tsx @@ -0,0 +1,43 @@ +import React, { forwardRef, useState } from 'react' +import { useAccessible } from '@metabuilder/fakemui/hooks' + +export interface StarButtonProps extends React.ButtonHTMLAttributes { + isStarred?: boolean + onToggleStar?: (starred: boolean) => void + testId?: string +} + +export const StarButton = forwardRef( + ({ 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) => { + const newState = !starred + setStarred(newState) + onToggleStar?.(newState) + props.onClick?.(e) + } + + return ( + + ) + } +) + +StarButton.displayName = 'StarButton' diff --git a/fakemui/react/components/email/atoms/index.ts b/fakemui/react/components/email/atoms/index.ts new file mode 100644 index 000000000..500865bf3 --- /dev/null +++ b/fakemui/react/components/email/atoms/index.ts @@ -0,0 +1,3 @@ +export { AttachmentIcon, type AttachmentIconProps } from './AttachmentIcon' +export { StarButton, type StarButtonProps } from './StarButton' +export { MarkAsReadCheckbox, type MarkAsReadCheckboxProps } from './MarkAsReadCheckbox' diff --git a/fakemui/react/components/email/data-display/AttachmentList.tsx b/fakemui/react/components/email/data-display/AttachmentList.tsx new file mode 100644 index 000000000..fdc911e40 --- /dev/null +++ b/fakemui/react/components/email/data-display/AttachmentList.tsx @@ -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( + ({ 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 ( + + {attachments.length === 0 ? ( + + No attachments + + ) : ( +
    + {attachments.map((attachment) => ( +
  • + +
    + {attachment.filename} + {formatFileSize(attachment.size)} +
    +
    + {attachment.downloadUrl && ( + + )} + +
    +
  • + ))} +
+ )} +
+ ) + } +) + +AttachmentList.displayName = 'AttachmentList' diff --git a/fakemui/react/components/email/data-display/EmailHeader.tsx b/fakemui/react/components/email/data-display/EmailHeader.tsx new file mode 100644 index 000000000..48530b67d --- /dev/null +++ b/fakemui/react/components/email/data-display/EmailHeader.tsx @@ -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( + ( + { + from, + to, + cc, + subject, + receivedAt, + isStarred = false, + onToggleStar, + testId: customTestId, + ...props + }, + ref + ) => { + const accessible = useAccessible({ + feature: 'email', + component: 'email-header', + identifier: customTestId || subject + }) + + return ( + +
+ + {subject} + + +
+
+ + From: {from} + + + To: {to.join(', ')} + + {cc && cc.length > 0 && ( + + Cc: {cc.join(', ')} + + )} + + {new Date(receivedAt).toLocaleString()} + +
+
+ ) + } +) + +EmailHeader.displayName = 'EmailHeader' diff --git a/fakemui/react/components/email/data-display/FolderTree.tsx b/fakemui/react/components/email/data-display/FolderTree.tsx new file mode 100644 index 000000000..73128d2c2 --- /dev/null +++ b/fakemui/react/components/email/data-display/FolderTree.tsx @@ -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( + ({ folders, selectedFolderId, onSelectFolder, testId: customTestId, ...props }, ref) => { + const [expandedFolders, setExpandedFolders] = useState>(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) => ( +
+ + {expandedFolders.has(folder.id) && folder.children && ( +
+ {folder.children.map((child) => renderFolder(child, level + 1))} +
+ )} +
+ ) + + return ( + + {folders.map((folder) => renderFolder(folder))} + + ) + } +) + +FolderTree.displayName = 'FolderTree' diff --git a/fakemui/react/components/email/data-display/ThreadList.tsx b/fakemui/react/components/email/data-display/ThreadList.tsx new file mode 100644 index 000000000..ea5807146 --- /dev/null +++ b/fakemui/react/components/email/data-display/ThreadList.tsx @@ -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> + selectedEmailId?: string + onSelectEmail?: (emailId: string) => void + onToggleRead?: (emailId: string, read: boolean) => void + onToggleStar?: (emailId: string, starred: boolean) => void + testId?: string +} + +export const ThreadList = forwardRef( + ( + { + emails, + selectedEmailId, + onSelectEmail, + onToggleRead, + onToggleStar, + testId: customTestId, + ...props + }, + ref + ) => { + const accessible = useAccessible({ + feature: 'email', + component: 'thread-list', + identifier: customTestId || 'threads' + }) + + return ( + + {emails.length === 0 ? ( +
No emails
+ ) : ( + emails.map((email, idx) => ( + onSelectEmail?.(email.testId || `email-${idx}`)} + onToggleRead={(read) => onToggleRead?.(email.testId || `email-${idx}`, read)} + onToggleStar={(starred) => onToggleStar?.(email.testId || `email-${idx}`, starred)} + /> + )) + )} +
+ ) + } +) + +ThreadList.displayName = 'ThreadList' diff --git a/fakemui/react/components/email/data-display/index.ts b/fakemui/react/components/email/data-display/index.ts new file mode 100644 index 000000000..a2896af3d --- /dev/null +++ b/fakemui/react/components/email/data-display/index.ts @@ -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' diff --git a/fakemui/react/components/email/feedback/SyncProgress.tsx b/fakemui/react/components/email/feedback/SyncProgress.tsx new file mode 100644 index 000000000..0d835824c --- /dev/null +++ b/fakemui/react/components/email/feedback/SyncProgress.tsx @@ -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( + ( + { + 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 ( + + + Syncing... {syncedMessages} of {totalMessages} messages + + + + {Math.round(progress)}% complete + + + ) + } +) + +SyncProgress.displayName = 'SyncProgress' diff --git a/fakemui/react/components/email/feedback/SyncStatusBadge.tsx b/fakemui/react/components/email/feedback/SyncStatusBadge.tsx new file mode 100644 index 000000000..7eb63af5f --- /dev/null +++ b/fakemui/react/components/email/feedback/SyncStatusBadge.tsx @@ -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( + ({ 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 ( + + + + ) + } +) + +SyncStatusBadge.displayName = 'SyncStatusBadge' diff --git a/fakemui/react/components/email/feedback/index.ts b/fakemui/react/components/email/feedback/index.ts new file mode 100644 index 000000000..2f02f6fa1 --- /dev/null +++ b/fakemui/react/components/email/feedback/index.ts @@ -0,0 +1,2 @@ +export { SyncStatusBadge, type SyncStatusBadgeProps, type SyncStatus } from './SyncStatusBadge' +export { SyncProgress, type SyncProgressProps } from './SyncProgress' diff --git a/fakemui/react/components/email/index.ts b/fakemui/react/components/email/index.ts new file mode 100644 index 000000000..33afc2878 --- /dev/null +++ b/fakemui/react/components/email/index.ts @@ -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' diff --git a/fakemui/react/components/email/inputs/BodyEditor.tsx b/fakemui/react/components/email/inputs/BodyEditor.tsx new file mode 100644 index 000000000..eb82da112 --- /dev/null +++ b/fakemui/react/components/email/inputs/BodyEditor.tsx @@ -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 { + mode?: 'plain' | 'html' + onModeChange?: (mode: 'plain' | 'html') => void + testId?: string +} + +export const BodyEditor = forwardRef( + ({ mode = 'plain', onModeChange, testId: customTestId, ...props }, ref) => { + const accessible = useAccessible({ + feature: 'email', + component: 'body-editor', + identifier: customTestId || 'body' + }) + + return ( + +
+ + +
+