diff --git a/components/fakemui/email/layout/EmailDetail.tsx b/components/fakemui/email/layout/EmailDetail.tsx index a36d90bb9..413a942c7 100644 --- a/components/fakemui/email/layout/EmailDetail.tsx +++ b/components/fakemui/email/layout/EmailDetail.tsx @@ -1,5 +1,6 @@ import React from 'react' import { Box, BoxProps, Button, IconButton } from '../..' +import { MaterialIcon } from '../../../../icons/react/fakemui' import { useAccessible } from '../../../../hooks/useAccessible' import { EmailHeader } from '../data-display' @@ -46,33 +47,29 @@ export const EmailDetail = ({ {onClose && ( - - ← + + )} {onArchive && ( - đŸ“Ĩ + )} {onDelete && ( - đŸ—‘ī¸ + )} {onReply && ( - â†Šī¸ + )} {onForward && ( - â†Ēī¸ + )} @@ -95,12 +92,14 @@ export const EmailDetail = ({ {onReply && ( )} {onForward && ( )} diff --git a/components/fakemui/email/layout/MailboxHeader.tsx b/components/fakemui/email/layout/MailboxHeader.tsx index 5290f8879..5ebeb3abf 100644 --- a/components/fakemui/email/layout/MailboxHeader.tsx +++ b/components/fakemui/email/layout/MailboxHeader.tsx @@ -1,26 +1,39 @@ import React from 'react' -import { Box, BoxProps, IconButton, Typography } from '../..' +import { Box, BoxProps, IconButton } from '../..' +import { MaterialIcon } from '../../../../icons/react/fakemui' import { useAccessible } from '../../../../hooks/useAccessible' export interface MailboxHeaderProps extends BoxProps { appName?: string - appIcon?: string searchQuery?: string onSearchChange?: (query: string) => void searchPlaceholder?: string avatarLabel?: string + isDarkMode?: boolean + onToggleTheme?: () => void + onMenuClick?: () => void onSettingsClick?: () => void + onAlertsClick?: () => void + alertCount?: number + locale?: string + onCycleLocale?: () => void testId?: string } export const MailboxHeader = ({ appName = 'MetaMail', - appIcon = '📧', searchQuery = '', onSearchChange, searchPlaceholder = 'Search mail', avatarLabel = 'U', + isDarkMode = false, + onToggleTheme, + onMenuClick, onSettingsClick, + onAlertsClick, + alertCount = 0, + locale = 'EN', + onCycleLocale, testId: customTestId, ...props }: MailboxHeaderProps) => { @@ -32,13 +45,20 @@ export const MailboxHeader = ({ return ( - - {appIcon} - - {appName} - + + {onMenuClick && ( + + + + )} + + + {appName} + + + + + {onAlertsClick && ( + + + + + {alertCount > 0 && {alertCount}} + + )} + {onToggleTheme && ( + + + + )} + {onCycleLocale && ( + + )} {onSettingsClick && ( - - âš™ī¸ + + )} diff --git a/components/fakemui/email/layout/MailboxSidebar.tsx b/components/fakemui/email/layout/MailboxSidebar.tsx index 9b84f715d..a91275073 100644 --- a/components/fakemui/email/layout/MailboxSidebar.tsx +++ b/components/fakemui/email/layout/MailboxSidebar.tsx @@ -1,5 +1,6 @@ import React from 'react' import { Box, BoxProps } from '../..' +import { MaterialIcon } from '../../../../icons/react/fakemui' import { useAccessible } from '../../../../hooks/useAccessible' import { FolderNavigation, type FolderNavigationItem } from '../navigation' @@ -30,7 +31,8 @@ export const MailboxSidebar = ({ {onCompose && ( )} diff --git a/frontends/emailclient/Dockerfile b/frontends/emailclient/Dockerfile index ac49447a0..7d0f67eaa 100644 --- a/frontends/emailclient/Dockerfile +++ b/frontends/emailclient/Dockerfile @@ -66,6 +66,7 @@ WORKDIR /app COPY --from=builder /app/frontends/emailclient/.next/standalone ./ COPY --from=builder /app/frontends/emailclient/.next/static ./frontends/emailclient/.next/static +COPY --from=builder /app/frontends/emailclient/public ./frontends/emailclient/public ENV NODE_ENV=production ENV PORT=3000 diff --git a/frontends/emailclient/app/EmailClientContent.tsx b/frontends/emailclient/app/EmailClientContent.tsx index 2ce7bfc1f..47767f4fb 100644 --- a/frontends/emailclient/app/EmailClientContent.tsx +++ b/frontends/emailclient/app/EmailClientContent.tsx @@ -17,12 +17,12 @@ import { Box, Typography } from '@metabuilder/fakemui' // ───────────────────────────────────────────────────────────────────────────── const DEMO_FOLDERS: FolderNavigationItem[] = [ - { id: 'inbox', label: 'Inbox', icon: 'đŸ“Ĩ', unreadCount: 3, isActive: true }, - { id: 'starred', label: 'Starred', icon: '⭐' }, - { id: 'sent', label: 'Sent', icon: '📤' }, - { id: 'drafts', label: 'Drafts', icon: '📝', unreadCount: 1 }, - { id: 'spam', label: 'Spam', icon: 'âš ī¸' }, - { id: 'trash', label: 'Trash', icon: 'đŸ—‘ī¸' }, + { id: 'inbox', label: 'Inbox', icon: 'inbox', unreadCount: 3, isActive: true }, + { id: 'starred', label: 'Starred', icon: 'star' }, + { id: 'sent', label: 'Sent', icon: 'send' }, + { id: 'drafts', label: 'Drafts', icon: 'drafts', unreadCount: 1 }, + { id: 'spam', label: 'Spam', icon: 'report' }, + { id: 'trash', label: 'Trash', icon: 'delete' }, ] const now = Date.now() @@ -115,6 +115,8 @@ export default function EmailClientContent() { const [emails, setEmails] = useState(DEMO_EMAILS) const [showCompose, setShowCompose] = useState(false) const [searchQuery, setSearchQuery] = useState('') + const [isDarkMode, setIsDarkMode] = useState(true) + const [sidebarOpen, setSidebarOpen] = useState(true) const folders = DEMO_FOLDERS.map(f => ({ ...f, @@ -181,7 +183,17 @@ export default function EmailClientContent() { { + setIsDarkMode(prev => !prev) + document.documentElement.setAttribute('data-theme', isDarkMode ? 'light' : 'dark') + }} + onMenuClick={() => setSidebarOpen(prev => !prev)} onSettingsClick={() => {}} + onAlertsClick={() => {}} + alertCount={unreadCount} + locale="EN" + onCycleLocale={() => {}} /> ) diff --git a/frontends/emailclient/app/globals.css b/frontends/emailclient/app/globals.css index 9ee5ec7e8..1c323428f 100644 --- a/frontends/emailclient/app/globals.css +++ b/frontends/emailclient/app/globals.css @@ -3,6 +3,36 @@ /* Uses FakeMUI M3 token system */ /* ============================================ */ +/* ============================================ */ +/* Material Symbols (self-hosted) */ +/* ============================================ */ +@font-face { + font-family: 'Material Symbols Outlined'; + font-style: normal; + font-weight: 100 700; + font-display: block; + src: url('/emailclient/fonts/material-symbols-outlined.woff2') format('woff2'); +} + +.material-symbols-outlined { + font-family: 'Material Symbols Outlined'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + font-feature-settings: 'liga' 1; + -webkit-font-feature-settings: 'liga' 1; + font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + *, *::before, *::after { box-sizing: border-box; margin: 0; @@ -74,10 +104,10 @@ } /* ============================================ */ -/* Dark Theme */ +/* Dark Theme (softer, less jarring) */ /* ============================================ */ @media (prefers-color-scheme: dark) { - :root { + :root:not([data-theme="light"]) { --mat-sys-primary: #d0bcff; --mat-sys-on-primary: #381e72; --mat-sys-primary-container: #4f378b; @@ -87,27 +117,76 @@ --mat-sys-secondary-container: #4a4458; --mat-sys-error: #f2b8b5; --mat-sys-on-error: #601410; - --mat-sys-surface: #141218; - --mat-sys-on-surface: #e6e0e9; - --mat-sys-surface-variant: #49454f; - --mat-sys-on-surface-variant: #cac4d0; - --mat-sys-surface-container-highest: #36343b; - --mat-sys-surface-container-high: #2b2930; - --mat-sys-surface-container: #211f26; - --mat-sys-surface-container-low: #1d1b20; - --mat-sys-surface-container-lowest: #0f0d13; - --mat-sys-surface-dim: #141218; - --mat-sys-surface-bright: #3b383e; - --mat-sys-background: #141218; - --mat-sys-on-background: #e6e0e9; - --mat-sys-outline: #938f99; - --mat-sys-outline-variant: #49454f; - --mat-sys-inverse-surface: #e6e0e9; - --mat-sys-inverse-on-surface: #322f35; + --mat-sys-surface: #1e1e2e; + --mat-sys-on-surface: #cdd6f4; + --mat-sys-surface-variant: #45475a; + --mat-sys-on-surface-variant: #bac2de; + --mat-sys-surface-container-highest: #45475a; + --mat-sys-surface-container-high: #363849; + --mat-sys-surface-container: #2a2b3d; + --mat-sys-surface-container-low: #232434; + --mat-sys-surface-container-lowest: #1a1b2e; + --mat-sys-surface-dim: #1e1e2e; + --mat-sys-surface-bright: #45475a; + --mat-sys-background: #1e1e2e; + --mat-sys-on-background: #cdd6f4; + --mat-sys-outline: #6c7086; + --mat-sys-outline-variant: #45475a; + --mat-sys-inverse-surface: #cdd6f4; + --mat-sys-inverse-on-surface: #1e1e2e; --mat-sys-inverse-primary: #6750a4; } } +/* Manual theme toggle */ +:root[data-theme="dark"] { + --mat-sys-primary: #d0bcff; + --mat-sys-on-primary: #381e72; + --mat-sys-primary-container: #4f378b; + --mat-sys-on-primary-container: #eaddff; + --mat-sys-secondary: #ccc2dc; + --mat-sys-on-secondary: #332d41; + --mat-sys-secondary-container: #4a4458; + --mat-sys-error: #f2b8b5; + --mat-sys-on-error: #601410; + --mat-sys-surface: #1e1e2e; + --mat-sys-on-surface: #cdd6f4; + --mat-sys-surface-variant: #45475a; + --mat-sys-on-surface-variant: #bac2de; + --mat-sys-surface-container-highest: #45475a; + --mat-sys-surface-container-high: #363849; + --mat-sys-surface-container: #2a2b3d; + --mat-sys-surface-container-low: #232434; + --mat-sys-surface-container-lowest: #1a1b2e; + --mat-sys-surface-dim: #1e1e2e; + --mat-sys-surface-bright: #45475a; + --mat-sys-background: #1e1e2e; + --mat-sys-on-background: #cdd6f4; + --mat-sys-outline: #6c7086; + --mat-sys-outline-variant: #45475a; + --mat-sys-inverse-surface: #cdd6f4; + --mat-sys-inverse-on-surface: #1e1e2e; + --mat-sys-inverse-primary: #6750a4; +} + +:root[data-theme="light"] { + --mat-sys-primary: #6750a4; + --mat-sys-on-primary: #ffffff; + --mat-sys-surface: #fef7ff; + --mat-sys-on-surface: #1d1b20; + --mat-sys-surface-variant: #e7e0ec; + --mat-sys-on-surface-variant: #49454f; + --mat-sys-surface-container-highest: #e6e0e9; + --mat-sys-surface-container-high: #ece6f0; + --mat-sys-surface-container: #f3edf7; + --mat-sys-surface-container-low: #f7f2fa; + --mat-sys-surface-container-lowest: #ffffff; + --mat-sys-background: #fef7ff; + --mat-sys-on-background: #1d1b20; + --mat-sys-outline: #79747e; + --mat-sys-outline-variant: #cac4d0; +} + /* ============================================ */ /* Base Styles */ /* ============================================ */ @@ -243,10 +322,13 @@ input, textarea, select { } .folder-nav-item .folder-icon { - font-size: 1.125rem; + font-family: 'Material Symbols Outlined'; + font-size: 20px; width: 24px; text-align: center; flex-shrink: 0; + font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20; + -webkit-font-smoothing: antialiased; } .folder-nav-item .folder-label { @@ -281,44 +363,64 @@ input, textarea, select { .mailbox-header-bar { display: flex; align-items: center; - gap: 12px; + gap: 8px; width: 100%; padding: 0 8px; } +.mailbox-header-left { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; + min-width: 200px; +} + .mailbox-header-brand { display: flex; align-items: center; - gap: 10px; + gap: 8px; flex-shrink: 0; } -.mailbox-header-icon { - font-size: 24px; +.mailbox-header-logo { + color: var(--mat-sys-primary); } .mailbox-header-title { - font-weight: 600 !important; - font-size: 1.25rem !important; + font-weight: 600; + font-size: 1.125rem; color: var(--mat-sys-on-surface); + white-space: nowrap; } .mailbox-header-search { flex: 1; max-width: 680px; margin: 0 auto; + position: relative; +} + +.mailbox-search-icon { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--mat-sys-on-surface-variant); + pointer-events: none; } .mailbox-search-input { width: 100%; - padding: 10px 16px; + padding: 8px 16px 8px 44px; border-radius: var(--mat-sys-corner-extra-large); border: none; background-color: var(--mat-sys-surface-container-high); color: var(--mat-sys-on-surface); - font-size: 0.9375rem; + font-size: 0.875rem; outline: none; - transition: background-color var(--mat-sys-motion-duration-short4) var(--mat-sys-motion-easing-standard); + transition: background-color var(--mat-sys-motion-duration-short4) var(--mat-sys-motion-easing-standard), + box-shadow var(--mat-sys-motion-duration-short4) var(--mat-sys-motion-easing-standard); } .mailbox-search-input:focus { @@ -333,12 +435,63 @@ input, textarea, select { .mailbox-header-actions { display: flex; align-items: center; - gap: 4px; + gap: 2px; flex-shrink: 0; } -.mailbox-header-action-icon { - font-size: 20px; +.mailbox-header-icon-btn { + color: var(--mat-sys-on-surface-variant) !important; + width: 36px !important; + height: 36px !important; + border-radius: var(--mat-sys-corner-full) !important; + transition: background-color var(--mat-sys-motion-duration-short4) !important; +} + +.mailbox-header-icon-btn:hover { + background-color: var(--mat-sys-surface-container-high) !important; +} + +.mailbox-header-alert-wrapper { + position: relative; + display: inline-flex; +} + +.mailbox-header-badge { + position: absolute; + top: 2px; + right: 2px; + min-width: 16px; + height: 16px; + border-radius: var(--mat-sys-corner-full); + background-color: var(--mat-sys-error); + color: var(--mat-sys-on-error); + font-size: 0.625rem; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + padding: 0 4px; + pointer-events: none; +} + +.mailbox-header-lang-btn { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 6px 10px; + border-radius: var(--mat-sys-corner-full); + background: none; + border: none; + color: var(--mat-sys-on-surface-variant); + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + transition: background-color var(--mat-sys-motion-duration-short4); + letter-spacing: 0.04em; +} + +.mailbox-header-lang-btn:hover { + background-color: var(--mat-sys-surface-container-high); } .mailbox-header-avatar { @@ -353,6 +506,7 @@ input, textarea, select { font-size: 0.875rem; font-weight: 600; cursor: pointer; + margin-left: 4px; } /* ============================================ */ diff --git a/frontends/emailclient/public/fonts/material-symbols-outlined.woff2 b/frontends/emailclient/public/fonts/material-symbols-outlined.woff2 new file mode 100644 index 000000000..67a5a18fb Binary files /dev/null and b/frontends/emailclient/public/fonts/material-symbols-outlined.woff2 differ