Enhance email components with dynamic accent colors for active tabs and improved styling. Updated CompactInboxCategoryTabs and EmailList to utilize new accent color logic, refined sidebar icons, and adjusted layout for better responsiveness in split view.

This commit is contained in:
R3D347HR4Y 2026-05-18 14:38:27 +02:00
parent 2eda0d4181
commit 18cb1257f4
6 changed files with 95 additions and 36 deletions

View File

@ -33,6 +33,7 @@ import {
DEFAULT_INBOX_TAB, DEFAULT_INBOX_TAB,
type MailRouteState, type MailRouteState,
} from "@/lib/mail-url" } from "@/lib/mail-url"
import { cn } from "@/lib/utils"
function segmentsFromPathname(pathname: string | null): string[] | undefined { function segmentsFromPathname(pathname: string | null): string[] | undefined {
if (!pathname?.startsWith("/mail")) return undefined if (!pathname?.startsWith("/mail")) return undefined
@ -120,7 +121,12 @@ function MailAppInner() {
/> />
</div> </div>
) : null} ) : null}
<div className="relative flex min-h-0 flex-1 gap-0 overflow-hidden bg-app-canvas pl-0 pr-0 pb-1 pt-1 sm:gap-1 sm:pl-1"> <div
className={cn(
"relative flex min-h-0 flex-1 gap-0 overflow-hidden pl-0 pr-0",
splitView ? "bg-white p-0" : "bg-app-canvas pb-1 pt-1 sm:gap-1 sm:pl-1"
)}
>
{!sidebarCollapsed && touchNav && ( {!sidebarCollapsed && touchNav && (
<button <button
type="button" type="button"
@ -145,7 +151,12 @@ function MailAppInner() {
folderUnreadCounts={folderUnreadCounts} folderUnreadCounts={folderUnreadCounts}
splitView={splitView} splitView={splitView}
/> />
<main className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-none bg-white shadow-sm sm:rounded-2xl"> <main
className={cn(
"flex min-h-0 flex-1 flex-col overflow-hidden bg-white",
splitView ? "rounded-none shadow-none" : "rounded-none shadow-sm sm:rounded-2xl"
)}
>
<Suspense> <Suspense>
<EmailList <EmailList
selectedFolder={route.folderId} selectedFolder={route.folderId}
@ -160,8 +171,15 @@ function MailAppInner() {
/> />
</Suspense> </Suspense>
</main> </main>
<div
className={cn(
"flex shrink-0 flex-col",
splitView && "border-l border-gray-200"
)}
>
<RightPanel /> <RightPanel />
</div> </div>
</div>
{!splitView ? ( {!splitView ? (
<MobileBottomBar <MobileBottomBar
sidebarOpen={!sidebarCollapsed} sidebarOpen={!sidebarCollapsed}

View File

@ -2,6 +2,7 @@
import { memo, useEffect, useLayoutEffect, useRef, useState } from "react" import { memo, useEffect, useLayoutEffect, useRef, useState } from "react"
import { Icon } from "@iconify/react" import { Icon } from "@iconify/react"
import { inboxTabActiveAccentColor } from "@/lib/inbox-category-tabs"
import { inboxTabShowsInactiveMeta } from "@/lib/mail-url" import { inboxTabShowsInactiveMeta } from "@/lib/mail-url"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -84,22 +85,31 @@ export const CompactInboxCategoryTabs = memo(function CompactInboxCategoryTabs({
} }
}, [displayedTabId, tabs]) }, [displayedTabId, tabs])
const activeTab = tabs.find((t) => t.id === displayedTabId)
const activeAccentColor = activeTab
? inboxTabActiveAccentColor(activeTab.id, activeTab.badgeColor)
: "#0b57d0"
return ( return (
<div ref={barRef} className="relative flex w-full min-w-0"> <div ref={barRef} className="relative flex w-full min-w-0">
<span <span
aria-hidden aria-hidden
className={cn( className={cn(
"pointer-events-none absolute bottom-0 left-0 z-[2] h-[3px] origin-left rounded-t-sm bg-[#0b57d0]", "pointer-events-none absolute bottom-0 left-0 z-[2] h-[3px] origin-left rounded-t-sm",
"will-change-[transform,width] transition-[transform,width] duration-300 ease-[cubic-bezier(0.2,0,0,1)] motion-reduce:transition-none", "will-change-[transform,width] transition-[transform,width,background-color] duration-300 ease-[cubic-bezier(0.2,0,0,1)] motion-reduce:transition-none",
indicator.ready ? "opacity-100" : "opacity-0" indicator.ready ? "opacity-100" : "opacity-0"
)} )}
style={{ style={{
width: indicator.width, width: indicator.width,
transform: `translate3d(${indicator.x}px, 0, 0)`, transform: `translate3d(${indicator.x}px, 0, 0)`,
backgroundColor: activeAccentColor,
}} }}
/> />
{tabs.map((tab) => { {tabs.map((tab) => {
const isActive = displayedTabId === tab.id const isActive = displayedTabId === tab.id
const accentColor = isActive
? inboxTabActiveAccentColor(tab.id, tab.badgeColor)
: undefined
const unseen = unseenInTabById[tab.id] ?? 0 const unseen = unseenInTabById[tab.id] ?? 0
const showMeta = const showMeta =
inboxTabShowsInactiveMeta(tab.id) && !isActive && unseen > 0 inboxTabShowsInactiveMeta(tab.id) && !isActive && unseen > 0
@ -135,8 +145,9 @@ export const CompactInboxCategoryTabs = memo(function CompactInboxCategoryTabs({
className={cn( className={cn(
TAB_ICON_CLASS, TAB_ICON_CLASS,
"transition-colors duration-200 motion-reduce:transition-none", "transition-colors duration-200 motion-reduce:transition-none",
isActive ? "text-[#0b57d0]" : "text-[#5f6368]" !isActive && "text-[#5f6368]"
)} )}
style={accentColor ? { color: accentColor } : undefined}
aria-hidden aria-hidden
/> />
{showMeta && unseen > 0 ? ( {showMeta && unseen > 0 ? (
@ -147,7 +158,10 @@ export const CompactInboxCategoryTabs = memo(function CompactInboxCategoryTabs({
) : null} ) : null}
</div> </div>
{isActive ? ( {isActive ? (
<span className="shrink-0 whitespace-nowrap text-[13px] font-semibold leading-tight text-[#0b57d0]"> <span
className="shrink-0 whitespace-nowrap text-[13px] font-semibold leading-tight"
style={{ color: accentColor }}
>
{tab.label} {tab.label}
</span> </span>
) : null} ) : null}

View File

@ -129,6 +129,7 @@ import {
} from "@/lib/mail-folder-display" } from "@/lib/mail-folder-display"
import { import {
buildInboxCategoryTabIcons, buildInboxCategoryTabIcons,
inboxTabActiveAccentColor,
resolveEmailInboxCategoryTabs, resolveEmailInboxCategoryTabs,
} from "@/lib/inbox-category-tabs" } from "@/lib/inbox-category-tabs"
import { import {
@ -2981,6 +2982,9 @@ export function EmailList({
> >
{inboxTabBarItems.map((tab) => { {inboxTabBarItems.map((tab) => {
const isActive = activeInboxTabId === tab.id const isActive = activeInboxTabId === tab.id
const accentColor = isActive
? inboxTabActiveAccentColor(tab.id, tab.badgeColor)
: undefined
const unseen = unseenInTabById[tab.id] ?? 0 const unseen = unseenInTabById[tab.id] ?? 0
const senderLine = tabUnseenSenderLineById[tab.id] ?? "" const senderLine = tabUnseenSenderLineById[tab.id] ?? ""
const showMeta = const showMeta =
@ -2994,11 +2998,15 @@ export function EmailList({
aria-label={tab.label} aria-label={tab.label}
aria-current={isActive ? "true" : undefined} aria-current={isActive ? "true" : undefined}
onClick={() => handleCategoryInboxTabClick(tab.id)} onClick={() => handleCategoryInboxTabClick(tab.id)}
style={
accentColor
? { boxShadow: `inset 0 -3px 0 0 ${accentColor}` }
: undefined
}
className={cn( className={cn(
"relative z-[1] flex cursor-pointer transition-colors", "relative z-[1] flex cursor-pointer transition-colors",
"min-w-0 w-full overflow-hidden max-sm:min-h-10 max-sm:items-center max-sm:justify-center", "min-w-0 w-full overflow-hidden max-sm:min-h-10 max-sm:items-center max-sm:justify-center",
"sm:min-h-14 sm:items-center sm:py-2 sm:text-left", "sm:min-h-14 sm:items-center sm:py-2 sm:text-left",
isActive && "shadow-[inset_0_-3px_0_0_#0b57d0]",
!isActive && "hover:bg-[#f1f3f4]" !isActive && "hover:bg-[#f1f3f4]"
)} )}
> >
@ -3009,8 +3017,9 @@ export function EmailList({
icon={tab.icon} icon={tab.icon}
className={cn( className={cn(
CATEGORY_TAB_ICON_CLASS, CATEGORY_TAB_ICON_CLASS,
isActive ? "text-[#0b57d0]" : "text-[#5f6368]" !isActive && "text-[#5f6368]"
)} )}
style={accentColor ? { color: accentColor } : undefined}
aria-hidden aria-hidden
/> />
{showMeta && unseen > 0 ? ( {showMeta && unseen > 0 ? (
@ -3028,8 +3037,9 @@ export function EmailList({
className={cn( className={cn(
CATEGORY_TAB_ICON_CLASS, CATEGORY_TAB_ICON_CLASS,
"self-center", "self-center",
isActive ? "text-[#0b57d0]" : "text-[#5f6368]" !isActive && "text-[#5f6368]"
)} )}
style={accentColor ? { color: accentColor } : undefined}
aria-hidden aria-hidden
/> />
<div className="flex min-w-0 w-0 flex-1 flex-col gap-px"> <div className="flex min-w-0 w-0 flex-1 flex-col gap-px">
@ -3042,8 +3052,9 @@ export function EmailList({
<span <span
className={cn( className={cn(
"min-w-0 flex-1 truncate text-[13px] font-semibold leading-tight", "min-w-0 flex-1 truncate text-[13px] font-semibold leading-tight",
isActive ? "text-[#0b57d0]" : "text-[#3c4043]" !isActive && "text-[#3c4043]"
)} )}
style={accentColor ? { color: accentColor } : undefined}
> >
{tab.label} {tab.label}
</span> </span>
@ -3080,8 +3091,7 @@ export function EmailList({
!splitView && isViewMode && openEmail !splitView && isViewMode && openEmail
? "relative flex min-h-0 flex-1 flex-col overflow-hidden" ? "relative flex min-h-0 flex-1 flex-col overflow-hidden"
: mainScrollClass, : mainScrollClass,
"relative min-h-0 flex-1 overscroll-y-none max-sm:pb-16", "relative min-h-0 flex-1 overscroll-y-none max-sm:pb-16"
listToolbarMode && "sm:pb-12"
)} )}
> >
{listToolbarMode && ( {listToolbarMode && (
@ -3286,7 +3296,7 @@ export function EmailList({
!splitView && !splitView &&
"md:flex md:items-start md:gap-2 md:px-2 md:py-1.5", "md:flex md:items-start md:gap-2 md:px-2 md:py-1.5",
isSplitActiveRow isSplitActiveRow
? "z-[1] bg-[#e8f0fe] shadow-[inset_3px_0_0_0_#0b57d0]" ? "z-[1] bg-[#e8f0fe] shadow-[inset_3px_0_0_0_#669df6]"
: isSelected : isSelected
? "bg-[#e8f0fe]" ? "bg-[#e8f0fe]"
: isRead : isRead
@ -4527,8 +4537,7 @@ export function EmailList({
</div> </div>
</div> </div>
{listToolbarMode ? ( {listToolbarMode ? (
<div className="pointer-events-none absolute bottom-0 left-0 z-20 hidden max-w-full sm:block"> <div className="hidden w-fit max-w-full shrink-0 self-start sm:block">
<div className="pointer-events-auto w-fit max-w-full">
<MailFolderStackIndicator <MailFolderStackIndicator
currentKey={mailNavVisitKey(selectedFolder, inboxTab)} currentKey={mailNavVisitKey(selectedFolder, inboxTab)}
folderTree={sidebarNav.folderTree} folderTree={sidebarNav.folderTree}
@ -4537,7 +4546,6 @@ export function EmailList({
onNavigate={handleBreadcrumbNavigate} onNavigate={handleBreadcrumbNavigate}
/> />
</div> </div>
</div>
) : null} ) : null}
</div> </div>

View File

@ -7,7 +7,6 @@ import {
ClockArrowUp, ClockArrowUp,
Send, Send,
FileText, FileText,
Tag,
ChevronDown, ChevronDown,
GripVertical, GripVertical,
Pencil, Pencil,
@ -141,7 +140,7 @@ const mainItems = [
{ id: "inbox", label: "Boîte de réception", icon: Inbox }, { id: "inbox", label: "Boîte de réception", icon: Inbox },
{ id: "starred", label: "Messages suivis", icon: Star }, { id: "starred", label: "Messages suivis", icon: Star },
{ id: "snoozed", label: "En attente", icon: Clock }, { id: "snoozed", label: "En attente", icon: Clock },
{ id: "important", label: "Important", icon: Tag }, { id: "important", label: "Important", icon: "mdi:label-variant-outline" },
{ id: "sent", label: "Messages envoyés", icon: Send }, { id: "sent", label: "Messages envoyés", icon: Send },
{ id: "drafts", label: "Brouillons", icon: FileText }, { id: "drafts", label: "Brouillons", icon: FileText },
{ id: "scheduled", label: "Planifié", icon: ClockArrowUp }, { id: "scheduled", label: "Planifié", icon: ClockArrowUp },
@ -1015,12 +1014,16 @@ export function Sidebar({
isSelected, isSelected,
unreadCount, unreadCount,
}: { }: {
item: { id: string; label: string; icon: React.ElementType } item: { id: string; label: string; icon: React.ElementType | string }
isSelected: boolean isSelected: boolean
unreadCount: number unreadCount: number
}) => { }) => {
const { isOver, dropHandlers } = useEmailDropTarget(item.id, item.label) const { isOver, dropHandlers } = useEmailDropTarget(item.id, item.label)
const hasUnread = unreadCount > 0 const hasUnread = unreadCount > 0
const iconClassName = cn(
"h-5 w-5 shrink-0",
hasUnread && !isSelected && "text-gray-900"
)
return ( return (
<button <button
onClick={() => onSelectFolder(item.id)} onClick={() => onSelectFolder(item.id)}
@ -1038,12 +1041,11 @@ export function Sidebar({
: "text-gray-700 hover:bg-gray-100" : "text-gray-700 hover:bg-gray-100"
)} )}
> >
<item.icon {typeof item.icon === "string" ? (
className={cn( <Icon icon={item.icon} className={iconClassName} aria-hidden />
"h-5 w-5 shrink-0", ) : (
hasUnread && !isSelected && "text-gray-900" <item.icon className={iconClassName} />
)} )}
/>
{isExpanded && ( {isExpanded && (
<div className="flex min-w-0 flex-1 items-baseline gap-4"> <div className="flex min-w-0 flex-1 items-baseline gap-4">
<span <span
@ -2450,6 +2452,7 @@ export function Sidebar({
className={cn( className={cn(
"absolute left-0 top-0 bottom-0 flex flex-col overflow-hidden bg-app-canvas transition-[width,transform] duration-200 z-40 select-none", "absolute left-0 top-0 bottom-0 flex flex-col overflow-hidden bg-app-canvas transition-[width,transform] duration-200 z-40 select-none",
isExpanded ? "w-60" : "w-[68px]", isExpanded ? "w-60" : "w-[68px]",
splitView && "border-r border-gray-200",
!touchNav && hoverExpanded && "shadow-xl border-r border-gray-200", !touchNav && hoverExpanded && "shadow-xl border-r border-gray-200",
isOverlayOpen && "z-50 shadow-xl border-r border-gray-200", isOverlayOpen && "z-50 shadow-xl border-r border-gray-200",
collapsed && isXs && "-translate-x-full pointer-events-none" collapsed && isXs && "-translate-x-full pointer-events-none"

View File

@ -1,10 +1,15 @@
import type { Email } from "@/lib/email-data" import type { Email } from "@/lib/email-data"
import { navFolderIconColorFromBgClass } from "@/lib/label-pill-contrast"
import { import {
emailMatchesFolder, emailMatchesFolder,
type MailFolderFilterCtx, type MailFolderFilterCtx,
type MailNavFolderMaps, type MailNavFolderMaps,
} from "@/lib/mail-folder-filter" } from "@/lib/mail-folder-filter"
import { DEFAULT_INBOX_TAB, INBOX_ALL_TAB } from "@/lib/mail-url" import {
DEFAULT_INBOX_TAB,
INBOX_ALL_TAB,
normalizeInboxTabSegment,
} from "@/lib/mail-url"
import { import {
tabbedInboxLabelRows, tabbedInboxLabelRows,
type LabelRowItem, type LabelRowItem,
@ -17,6 +22,15 @@ export type InboxCategoryTabIcon = {
badgeColor: string badgeColor: string
} }
/** Couleur icône / libellé / soulignement de longlet boîte actif. */
export function inboxTabActiveAccentColor(
tabId: string,
badgeColor: string
): string {
if (normalizeInboxTabSegment(tabId) === INBOX_ALL_TAB) return "#202124"
return navFolderIconColorFromBgClass(badgeColor)
}
/** Onglets catégorie boîte (Principale + libellés tabbed), hors « Tous les messages ». */ /** Onglets catégorie boîte (Principale + libellés tabbed), hors « Tous les messages ». */
export function buildInboxCategoryTabIcons( export function buildInboxCategoryTabIcons(
labelRows: readonly LabelRowItem[] labelRows: readonly LabelRowItem[]

View File

@ -3,7 +3,6 @@ import {
Inbox, Inbox,
Star, Star,
Clock, Clock,
Tag,
Send, Send,
FileText, FileText,
ClockArrowUp, ClockArrowUp,
@ -26,7 +25,6 @@ const SYSTEM_ICONS: Record<string, LucideIcon> = {
inbox: Inbox, inbox: Inbox,
starred: Star, starred: Star,
snoozed: Clock, snoozed: Clock,
important: Tag,
sent: Send, sent: Send,
drafts: FileText, drafts: FileText,
scheduled: ClockArrowUp, scheduled: ClockArrowUp,
@ -55,6 +53,10 @@ export function resolveMailNavIcon(
return { kind: "lucide", Icon: Inbox } return { kind: "lucide", Icon: Inbox }
} }
if (folderId === "important") {
return { kind: "iconify", icon: "mdi:label-variant-outline" }
}
const system = SYSTEM_ICONS[folderId] const system = SYSTEM_ICONS[folderId]
if (system) return { kind: "lucide", Icon: system } if (system) return { kind: "lucide", Icon: system }