fix(emailclient): 10-issue polish pass — visual feedback, state sync, UX

- Selected email card: add secondary-container bg + left accent border
- Unread email cards: add primary left border accent
- StarButton: sync local state with parent via useEffect([isStarred])
- Initial dark mode: useEffect applies data-theme on mount
- Star active color: amber #f9a825 via .star-button--active CSS rule
- Unread count badge: styled as pill with primary-container background
- Empty state: add inbox/folder_open material icon
- Folder toolbar label: capitalize + replace underscores with spaces
- ComposeWindow: CC/BCC hidden by default, revealed via Cc/Bcc button
- Email header: flex layout for .header-top, column gap for .header-details

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 21:02:32 +00:00
parent a50cc947fe
commit 33be8aa9db
4 changed files with 92 additions and 15 deletions

View File

@@ -1,5 +1,5 @@
// fakemui/email/atoms/StarButton.tsx // fakemui/email/atoms/StarButton.tsx
import React, { forwardRef, useState } from 'react' import React, { forwardRef, useEffect, useState } from 'react'
import { MaterialIcon } from '../../../../icons/react/fakemui' import { MaterialIcon } from '../../../../icons/react/fakemui'
import { useAccessible } from '../../../../hooks/useAccessible' import { useAccessible } from '../../../../hooks/useAccessible'
@@ -16,6 +16,8 @@ export const StarButton = forwardRef<
>(({ isStarred = false, onToggleStar, testId, ...props }, ref) => { >(({ isStarred = false, onToggleStar, testId, ...props }, ref) => {
const [starred, setStarred] = useState(isStarred) const [starred, setStarred] = useState(isStarred)
useEffect(() => { setStarred(isStarred) }, [isStarred])
const accessible = useAccessible({ const accessible = useAccessible({
feature: 'email', feature: 'email',
component: 'star-button', component: 'star-button',

View File

@@ -24,6 +24,7 @@ export const ComposeWindow = ({
const [bcc, setBcc] = useState<string[]>([]) const [bcc, setBcc] = useState<string[]>([])
const [subject, setSubject] = useState('') const [subject, setSubject] = useState('')
const [body, setBody] = useState('') const [body, setBody] = useState('')
const [showCcBcc, setShowCcBcc] = useState(false)
const accessible = useAccessible({ const accessible = useAccessible({
feature: 'email', component: 'compose', feature: 'email', component: 'compose',
identifier: customTestId || 'compose' identifier: customTestId || 'compose'
@@ -58,15 +59,37 @@ export const ComposeWindow = ({
</button> </button>
</Box> </Box>
<Box className="compose-body"> <Box className="compose-body">
<RecipientInput recipientType="to" <Box style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
recipients={to} onRecipientsChange={setTo} <RecipientInput recipientType="to"
placeholder="To:" /> recipients={to} onRecipientsChange={setTo}
<RecipientInput recipientType="cc" placeholder="To:" />
recipients={cc} onRecipientsChange={setCc} {!showCcBcc && (
placeholder="Cc:" /> <button
<RecipientInput recipientType="bcc" type="button"
recipients={bcc} onRecipientsChange={setBcc} onClick={() => setShowCcBcc(true)}
placeholder="Bcc:" /> style={{
fontSize: '0.75rem',
color: 'var(--mat-sys-on-surface-variant)',
whiteSpace: 'nowrap',
flexShrink: 0,
padding: '4px 6px',
}}
aria-label="Show Cc and Bcc fields"
>
Cc/Bcc
</button>
)}
</Box>
{showCcBcc && (
<>
<RecipientInput recipientType="cc"
recipients={cc} onRecipientsChange={setCc}
placeholder="Cc:" />
<RecipientInput recipientType="bcc"
recipients={bcc} onRecipientsChange={setBcc}
placeholder="Bcc:" />
</>
)}
<input type="text" placeholder="Subject" <input type="text" placeholder="Subject"
value={subject} id="compose-subject" value={subject} id="compose-subject"
aria-label="Subject" aria-label="Subject"

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import React from 'react' import React, { useEffect } from 'react'
import { import {
MailboxLayout, MailboxLayout,
MailboxHeader, MailboxHeader,
@@ -15,6 +15,13 @@ import { useEmailClient } from './hooks/useEmailClient'
export default function EmailClientContent() { export default function EmailClientContent() {
const { state, actions } = useEmailClient() const { state, actions } = useEmailClient()
useEffect(() => {
document.documentElement.setAttribute(
'data-theme',
state.isDarkMode ? 'dark' : 'light'
)
}, [state.isDarkMode])
const header = ( const header = (
<MailboxHeader <MailboxHeader
searchQuery={state.searchQuery} searchQuery={state.searchQuery}
@@ -53,7 +60,9 @@ export default function EmailClientContent() {
variant="body2" variant="body2"
className="mailbox-thread-folder-label" className="mailbox-thread-folder-label"
> >
{state.activeFolder} {state.activeFolder
.replace(/_/g, ' ')
.replace(/^\w/, c => c.toUpperCase())}
{state.filteredEmails.length > 0 && {state.filteredEmails.length > 0 &&
` (${state.filteredEmails.length})`} ` (${state.filteredEmails.length})`}
</Typography> </Typography>
@@ -66,6 +75,11 @@ export default function EmailClientContent() {
</Box> </Box>
{state.filteredEmails.length === 0 ? ( {state.filteredEmails.length === 0 ? (
<Box className="mailbox-empty-state"> <Box className="mailbox-empty-state">
<span className="material-symbols-outlined mailbox-empty-icon">
{state.activeFolder === 'inbox' || state.searchQuery
? 'inbox'
: 'folder_open'}
</span>
<Typography variant="body2"> <Typography variant="body2">
{state.activeFolder === 'inbox' && {state.activeFolder === 'inbox' &&
state.searchQuery state.searchQuery

View File

@@ -339,10 +339,15 @@ input, textarea, select {
} }
.folder-nav-item .unread-count { .folder-nav-item .unread-count {
font-size: 0.75rem; background-color: var(--mat-sys-primary-container);
font-weight: 600; color: var(--mat-sys-on-primary-container);
border-radius: var(--mat-sys-corner-full);
padding: 0 6px;
font-size: 0.6875rem;
font-weight: 700;
min-width: 18px;
text-align: center;
margin-left: auto; margin-left: auto;
color: var(--mat-sys-on-surface-variant);
} }
/* Active folder — filled tonal */ /* Active folder — filled tonal */
@@ -616,8 +621,17 @@ input, textarea, select {
border-color: var(--mat-sys-surface-variant); border-color: var(--mat-sys-surface-variant);
} }
/* Issue 1 — selected card distinct highlight */
.email-card[aria-current="true"] {
background-color: var(--mat-sys-secondary-container) !important;
border-left: 3px solid var(--mat-sys-primary);
padding-left: 13px;
}
.email-card--unread { .email-card--unread {
background-color: var(--mat-sys-surface-container-lowest); background-color: var(--mat-sys-surface-container-lowest);
border-left: 3px solid var(--mat-sys-primary);
padding-left: 13px;
} }
.email-card--unread .email-from, .email-card--unread .email-from,
@@ -698,6 +712,15 @@ input, textarea, select {
background-color: var(--mat-sys-surface-container-high); background-color: var(--mat-sys-surface-container-high);
} }
/* Issue 4 — starred state amber color */
.star-button--active {
color: #f9a825;
}
.star-button--active .material-symbols-outlined {
color: #f9a825;
}
/* ============================================ */ /* ============================================ */
/* Email Detail / Reading Pane */ /* Email Detail / Reading Pane */
/* ============================================ */ /* ============================================ */
@@ -714,6 +737,21 @@ input, textarea, select {
line-height: 1.3; line-height: 1.3;
} }
/* Issue 9 — email header metadata layout */
.header-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
}
.header-details {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 12px;
}
/* ============================================ */ /* ============================================ */
/* Thread Panel (main area) */ /* Thread Panel (main area) */
/* ============================================ */ /* ============================================ */