mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
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:
@@ -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',
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) */
|
||||||
/* ============================================ */
|
/* ============================================ */
|
||||||
|
|||||||
Reference in New Issue
Block a user