Refactor email components to enhance user experience with improved category tab icons and swipe functionality. Added new MailInboxCategoryTabIcons and MailListSwipeRow components, updated inbox tab handling, and refined styles for better visual consistency.
This commit is contained in:
parent
4207b5eb55
commit
a48823cf1e
@ -2,6 +2,7 @@
|
||||
|
||||
import { memo, useEffect, useLayoutEffect, useRef, useState } from "react"
|
||||
import { Icon } from "@iconify/react"
|
||||
import { inboxTabShowsInactiveMeta } from "@/lib/mail-url"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type CompactInboxCategoryTab = {
|
||||
@ -99,9 +100,9 @@ export const CompactInboxCategoryTabs = memo(function CompactInboxCategoryTabs({
|
||||
/>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = displayedTabId === tab.id
|
||||
const isPrimaryTab = tab.id === "primary"
|
||||
const unseen = unseenInTabById[tab.id] ?? 0
|
||||
const showMeta = !isPrimaryTab && !isActive && unseen > 0
|
||||
const showMeta =
|
||||
inboxTabShowsInactiveMeta(tab.id) && !isActive && unseen > 0
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@ -307,7 +307,7 @@ function RecipientField({
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-6 w-6 items-center justify-center rounded-full text-[10px] font-medium text-white",
|
||||
"flex h-6 w-6 items-center justify-center rounded-full text-[10px] font-bold text-white",
|
||||
getColor(c.email)
|
||||
)}
|
||||
>
|
||||
@ -367,7 +367,7 @@ function RecipientField({
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-medium text-white",
|
||||
"flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-bold text-white",
|
||||
getColor(s.email)
|
||||
)}
|
||||
>
|
||||
|
||||
@ -150,7 +150,7 @@ export function ContactHoverCard({
|
||||
<div className="p-4 pb-3">
|
||||
<div className="relative flex items-start gap-3">
|
||||
<div
|
||||
className="flex h-14 w-14 shrink-0 items-center justify-center rounded-full text-lg font-medium text-white"
|
||||
className="flex h-14 w-14 shrink-0 items-center justify-center rounded-full text-lg font-bold text-white"
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
{senderInitial(name)}
|
||||
|
||||
@ -93,6 +93,7 @@ import {
|
||||
EmptyTitle,
|
||||
} from "@/components/ui/empty"
|
||||
import { CompactInboxCategoryTabs } from "@/components/gmail/compact-inbox-category-tabs"
|
||||
import { MailInboxCategoryTabIcons } from "@/components/gmail/mail-inbox-category-tab-icons"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { labelPillTextClassForTailwindBgUtility } from "@/lib/label-pill-contrast"
|
||||
import {
|
||||
@ -119,7 +120,6 @@ import { cleanSenderName, resolveSenderEmail } from "@/lib/sender-display"
|
||||
import {
|
||||
getMailNavFolderLabel,
|
||||
inboxTabDisplayLabel,
|
||||
tabbedInboxLabelRows,
|
||||
type FolderTreeNode,
|
||||
type LabelRowItem,
|
||||
} from "@/lib/sidebar-nav-data"
|
||||
@ -127,7 +127,16 @@ import {
|
||||
mailNavVisitKey,
|
||||
parseMailNavVisitKey,
|
||||
} from "@/lib/mail-folder-display"
|
||||
import { DEFAULT_INBOX_TAB, normalizeInboxTabSegment } from "@/lib/mail-url"
|
||||
import {
|
||||
buildInboxCategoryTabIcons,
|
||||
resolveEmailInboxCategoryTabs,
|
||||
} from "@/lib/inbox-category-tabs"
|
||||
import {
|
||||
DEFAULT_INBOX_TAB,
|
||||
INBOX_ALL_TAB,
|
||||
inboxTabShowsInactiveMeta,
|
||||
normalizeInboxTabSegment,
|
||||
} from "@/lib/mail-url"
|
||||
import { MailFolderStackIndicator } from "@/components/gmail/mail-folder-stack-indicator"
|
||||
import { useSidebarNav, registerNavEmailSync } from "@/lib/sidebar-nav-context"
|
||||
import { ContactHoverCard } from "./contact-hover-card"
|
||||
@ -135,6 +144,7 @@ import { EmailLabelPickerBlock } from "@/components/gmail/email-label-picker-blo
|
||||
import type { CatalogLabelPresence } from "@/components/gmail/email-label-picker-block"
|
||||
import { resolveLabelPickerVisual } from "@/lib/label-picker-visual"
|
||||
import { MobileXsBulkSheets } from "@/components/gmail/mobile-xs-bulk-sheets"
|
||||
import { MailListSwipeRow } from "@/components/gmail/mail-list-swipe-row"
|
||||
import {
|
||||
useMoveTargets,
|
||||
type MoveTarget,
|
||||
@ -158,6 +168,7 @@ import {
|
||||
import type { LabelEditState } from "@/lib/stores/mail-store"
|
||||
import type { MailRouteState } from "@/lib/mail-url"
|
||||
import { readXsMatches, useIsXs } from "@/hooks/use-xs"
|
||||
import { useTouchNav } from "@/hooks/use-touch-nav"
|
||||
|
||||
addCollection(mdiIcons)
|
||||
|
||||
@ -315,18 +326,13 @@ type InboxTabBarItem = {
|
||||
|
||||
function buildInboxTabBarItems(labelRows: readonly LabelRowItem[]): InboxTabBarItem[] {
|
||||
return [
|
||||
...buildInboxCategoryTabIcons(labelRows),
|
||||
{
|
||||
id: "primary",
|
||||
label: "Principale",
|
||||
id: INBOX_ALL_TAB,
|
||||
label: "Tous les messages",
|
||||
icon: "mdi:inbox",
|
||||
badgeColor: "bg-[#0b57d0]",
|
||||
},
|
||||
...tabbedInboxLabelRows(labelRows).map((r) => ({
|
||||
id: r.id,
|
||||
label: r.label,
|
||||
icon: r.icon ?? "mdi:label-outline",
|
||||
badgeColor: r.color,
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
@ -637,6 +643,11 @@ export function EmailList({
|
||||
[sidebarNav.folderIdToLabel, sidebarNav.folderTree, sidebarNav.labelRows]
|
||||
)
|
||||
|
||||
const inboxCategoryTabIconsCatalog = useMemo(
|
||||
() => buildInboxCategoryTabIcons(sidebarNav.labelRows),
|
||||
[sidebarNav.labelRows]
|
||||
)
|
||||
|
||||
const inboxTabBarItems = useMemo(
|
||||
() => buildInboxTabBarItems(sidebarNav.labelRows),
|
||||
[sidebarNav.labelRows]
|
||||
@ -758,7 +769,10 @@ export function EmailList({
|
||||
const [mobileXsMoreMenuOpen, setMobileXsMoreMenuOpen] = useState(false)
|
||||
const [mobileXsMoveSheetOpen, setMobileXsMoveSheetOpen] = useState(false)
|
||||
const [mobileXsLabelSheetOpen, setMobileXsLabelSheetOpen] = useState(false)
|
||||
const [swipeLabelEmailId, setSwipeLabelEmailId] = useState<string | null>(null)
|
||||
const [openSwipeRowId, setOpenSwipeRowId] = useState<string | null>(null)
|
||||
const isXs = useIsXs()
|
||||
const touchNav = useTouchNav()
|
||||
|
||||
const openMobileXsMoveSheet = useCallback(() => {
|
||||
setMobileXsMoreMenuOpen(false)
|
||||
@ -775,8 +789,29 @@ export function EmailList({
|
||||
|
||||
const openMobileXsLabelSheet = useCallback(() => {
|
||||
setMobileXsMoreMenuOpen(false)
|
||||
setSwipeLabelEmailId(null)
|
||||
window.setTimeout(() => setMobileXsLabelSheetOpen(true), 0)
|
||||
}, [])
|
||||
|
||||
const handleLabelSheetOpenChange = useCallback((open: boolean) => {
|
||||
setMobileXsLabelSheetOpen(open)
|
||||
if (!open) setSwipeLabelEmailId(null)
|
||||
}, [])
|
||||
|
||||
const touchListSwipeEnabled = touchNav && !mobileSelectionMode && !isViewMode
|
||||
|
||||
useEffect(() => {
|
||||
if (!openSwipeRowId) return
|
||||
const handler = (e: globalThis.TouchEvent) => {
|
||||
const target = e.target as HTMLElement | null
|
||||
if (!target) return
|
||||
const swipeRow = target.closest(`[data-swipe-row-id="${openSwipeRowId}"]`)
|
||||
if (!swipeRow) setOpenSwipeRowId(null)
|
||||
}
|
||||
document.addEventListener("touchstart", handler, { passive: true })
|
||||
return () => document.removeEventListener("touchstart", handler)
|
||||
}, [openSwipeRowId])
|
||||
|
||||
const listViewportRef = useRef<HTMLDivElement>(null)
|
||||
const pullContentRef = useRef<HTMLDivElement>(null)
|
||||
const pullIconRef = useRef<SVGSVGElement>(null)
|
||||
@ -927,7 +962,7 @@ export function EmailList({
|
||||
subtreeIdsCache
|
||||
)
|
||||
)
|
||||
} else {
|
||||
} else if (tab !== INBOX_ALL_TAB) {
|
||||
rows = rows.filter(
|
||||
(email) =>
|
||||
emailMatchesFolder(
|
||||
@ -1016,12 +1051,38 @@ export function EmailList({
|
||||
ReturnType<typeof resolveParsedCalendarInvitation>
|
||||
>()
|
||||
const attachmentsById = new Map<string, EmailAttachment[]>()
|
||||
const categoryTabsById = new Map<
|
||||
string,
|
||||
ReturnType<typeof resolveEmailInboxCategoryTabs>
|
||||
>()
|
||||
const subtreeIdsCache = new Map<string, string[] | null>()
|
||||
const showCategoryTabIcons =
|
||||
selectedFolder === "inbox" &&
|
||||
normalizeInboxTabSegment(inboxTab) === INBOX_ALL_TAB
|
||||
|
||||
for (const e of listEmails) {
|
||||
invitationById.set(e.id, resolveParsedCalendarInvitation(e))
|
||||
attachmentsById.set(e.id, attachmentsForEmailList(e))
|
||||
if (showCategoryTabIcons) {
|
||||
const tabs = resolveEmailInboxCategoryTabs(
|
||||
e,
|
||||
folderFilterCtx,
|
||||
navMaps,
|
||||
inboxCategoryTabIconsCatalog,
|
||||
subtreeIdsCache
|
||||
)
|
||||
if (tabs.length > 0) categoryTabsById.set(e.id, tabs)
|
||||
}
|
||||
return { invitationById, attachmentsById }
|
||||
}, [listEmails])
|
||||
}
|
||||
return { invitationById, attachmentsById, categoryTabsById }
|
||||
}, [
|
||||
listEmails,
|
||||
selectedFolder,
|
||||
inboxTab,
|
||||
folderFilterCtx,
|
||||
navMaps,
|
||||
inboxCategoryTabIconsCatalog,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (isXs) return
|
||||
@ -1473,6 +1534,9 @@ export function EmailList({
|
||||
!seen.has(e.id)
|
||||
)
|
||||
}
|
||||
if (tab.id === INBOX_ALL_TAB) {
|
||||
return !seen.has(e.id)
|
||||
}
|
||||
return (
|
||||
emailMatchesFolder(e, "inbox", folderFilterCtx, navMaps, tabCache) &&
|
||||
emailMatchesFolder(e, tab.id, folderFilterCtx, navMaps, tabCache) &&
|
||||
@ -1480,6 +1544,7 @@ export function EmailList({
|
||||
)
|
||||
})
|
||||
counts[tab.id] = rows.length
|
||||
if (inboxTabShowsInactiveMeta(tab.id)) {
|
||||
const chain: string[] = []
|
||||
const used = new Set<string>()
|
||||
for (const e of rows) {
|
||||
@ -1491,6 +1556,7 @@ export function EmailList({
|
||||
}
|
||||
preview[tab.id] = chain.join(", ")
|
||||
}
|
||||
}
|
||||
return { unseenInTabById: counts, tabUnseenSenderLineById: preview }
|
||||
}, [folderFilterCtx, hiddenEmailIds, labelEdits, seenSerialized, allEmails, navMaps, notSpamEmailIds, inboxTabBarItems])
|
||||
|
||||
@ -1542,6 +1608,11 @@ export function EmailList({
|
||||
}, [bulkTargetIds, readOverrides, allEmails])
|
||||
const showBulkToolbar = bulkTargetIds.length > 0
|
||||
|
||||
const labelSheetTargetIds = useMemo(
|
||||
() => (swipeLabelEmailId ? [swipeLabelEmailId] : bulkTargetIds),
|
||||
[swipeLabelEmailId, bulkTargetIds]
|
||||
)
|
||||
|
||||
const clearBulkSelection = (ids: string[]) => {
|
||||
setSelectedEmails((prev) => prev.filter((id) => !ids.includes(id)))
|
||||
}
|
||||
@ -1713,6 +1784,35 @@ export function EmailList({
|
||||
[openMailId, goBack]
|
||||
)
|
||||
|
||||
const archiveListRow = useCallback(
|
||||
(email: Email) => {
|
||||
if (email.labels?.includes("scheduled")) {
|
||||
void requestArchiveScheduled(email.id)
|
||||
} else {
|
||||
mailActions.hideEmail(email.id)
|
||||
closeViewIfShowingEmail(email.id)
|
||||
}
|
||||
},
|
||||
[closeViewIfShowingEmail, mailActions, requestArchiveScheduled]
|
||||
)
|
||||
|
||||
const deleteListRow = useCallback(
|
||||
(email: Email) => {
|
||||
if (email.labels?.includes("scheduled")) {
|
||||
void requestDeleteScheduled(email.id)
|
||||
} else {
|
||||
mailActions.hideEmail(email.id)
|
||||
closeViewIfShowingEmail(email.id)
|
||||
}
|
||||
},
|
||||
[closeViewIfShowingEmail, mailActions, requestDeleteScheduled]
|
||||
)
|
||||
|
||||
const openSwipeRowLabelSheet = useCallback((emailId: string) => {
|
||||
setSwipeLabelEmailId(emailId)
|
||||
setMobileXsLabelSheetOpen(true)
|
||||
}, [])
|
||||
|
||||
const restoreSnoozedRowToMailbox = useCallback(
|
||||
(emailRow: Email) => {
|
||||
void requestRestoreSnoozedToInbox(emailRow)
|
||||
@ -2477,25 +2577,27 @@ export function EmailList({
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
{!isViewMode && touchNav && (
|
||||
<MobileXsBulkSheets
|
||||
moveSheetOpen={mobileXsMoveSheetOpen}
|
||||
moveSheetOpen={isXs && mobileXsMoveSheetOpen}
|
||||
onMoveSheetOpenChange={handleMobileXsMoveSheetOpenChange}
|
||||
labelSheetOpen={mobileXsLabelSheetOpen}
|
||||
onLabelSheetOpenChange={setMobileXsLabelSheetOpen}
|
||||
onLabelSheetOpenChange={handleLabelSheetOpenChange}
|
||||
labelPickerQuery={labelPickerQuery}
|
||||
onLabelPickerQueryChange={setLabelPickerQuery}
|
||||
catalogLabels={catalogLabels}
|
||||
resolveLabelVisual={resolveLabelVisual}
|
||||
moveTargets={moveTargets}
|
||||
onMoveTo={bulkMoveTo}
|
||||
getLabelPresence={(lab) => getCatalogLabelPresence(bulkTargetIds, lab)}
|
||||
onToggleCatalogLabel={(lab) => toggleLabelOnEmails(bulkTargetIds, lab)}
|
||||
getLabelPresence={(lab) => getCatalogLabelPresence(labelSheetTargetIds, lab)}
|
||||
onToggleCatalogLabel={(lab) => toggleLabelOnEmails(labelSheetTargetIds, lab)}
|
||||
onCreateLabel={(lab) => {
|
||||
addLabelToEmails(bulkTargetIds, lab)
|
||||
addLabelToEmails(labelSheetTargetIds, lab)
|
||||
setLabelPickerQuery("")
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={cn("flex min-h-0 flex-1 flex-col", splitView && "min-h-0 flex-row overflow-hidden")}>
|
||||
@ -2879,11 +2981,10 @@ export function EmailList({
|
||||
>
|
||||
{inboxTabBarItems.map((tab) => {
|
||||
const isActive = activeInboxTabId === tab.id
|
||||
const isPrimaryTab = tab.id === "primary"
|
||||
const unseen = unseenInTabById[tab.id] ?? 0
|
||||
const senderLine = tabUnseenSenderLineById[tab.id] ?? ""
|
||||
const showMeta =
|
||||
!isPrimaryTab && !isActive && unseen > 0
|
||||
inboxTabShowsInactiveMeta(tab.id) && !isActive && unseen > 0
|
||||
const showSenderLine = showMeta && Boolean(senderLine)
|
||||
const isExpandedTabMeta = showSenderLine
|
||||
return (
|
||||
@ -3152,6 +3253,19 @@ export function EmailList({
|
||||
}}
|
||||
>
|
||||
<ContextMenuTrigger asChild>
|
||||
<MailListSwipeRow
|
||||
enabled={touchListSwipeEnabled}
|
||||
emailId={email.id}
|
||||
isOpen={openSwipeRowId === email.id}
|
||||
onOpenChange={(open) => {
|
||||
if (open) setOpenSwipeRowId(email.id)
|
||||
else if (openSwipeRowId === email.id) setOpenSwipeRowId(null)
|
||||
}}
|
||||
onArchive={() => archiveListRow(email)}
|
||||
onDelete={() => deleteListRow(email)}
|
||||
onStar={() => toggleStar(email.id)}
|
||||
onLabel={() => openSwipeRowLabelSheet(email.id)}
|
||||
>
|
||||
<div
|
||||
data-email-row-id={email.id}
|
||||
data-split-active={isSplitActiveRow ? "" : undefined}
|
||||
@ -3324,6 +3438,12 @@ export function EmailList({
|
||||
aria-label="Pièces jointes"
|
||||
/>
|
||||
)}
|
||||
{listRowExtras.categoryTabsById.get(email.id) ? (
|
||||
<MailInboxCategoryTabIcons
|
||||
tabs={listRowExtras.categoryTabsById.get(email.id)!}
|
||||
onTabClick={handleCategoryInboxTabClick}
|
||||
/>
|
||||
) : null}
|
||||
<span
|
||||
className={cn(
|
||||
"shrink-0 text-sm font-semibold tabular-nums tracking-tight",
|
||||
@ -3875,6 +3995,13 @@ export function EmailList({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{listRowExtras.categoryTabsById.get(email.id) ? (
|
||||
<MailInboxCategoryTabIcons
|
||||
tabs={listRowExtras.categoryTabsById.get(email.id)!}
|
||||
onTabClick={handleCategoryInboxTabClick}
|
||||
iconClassName="size-[18px] shrink-0"
|
||||
/>
|
||||
) : null}
|
||||
<span
|
||||
className={cn(
|
||||
"min-w-0 truncate text-sm tabular-nums",
|
||||
@ -4049,6 +4176,7 @@ export function EmailList({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MailListSwipeRow>
|
||||
</ContextMenuTrigger>
|
||||
|
||||
<ContextMenuContent
|
||||
|
||||
@ -448,7 +448,7 @@ function CollapsedMessage({
|
||||
className="group flex w-full cursor-pointer items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-[#f6f9fe]"
|
||||
>
|
||||
<div
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-sm font-medium text-white"
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-sm font-bold text-white"
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
{senderInitial(name)}
|
||||
@ -522,7 +522,7 @@ function ExpandedMessage({
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex h-10 w-10 shrink-0 self-start items-center justify-center rounded-full text-sm font-medium text-white"
|
||||
className="flex h-10 w-10 shrink-0 self-start items-center justify-center rounded-full text-sm font-bold text-white"
|
||||
style={{ backgroundColor: avatarColor(name) }}
|
||||
>
|
||||
{senderInitial(name)}
|
||||
@ -1032,7 +1032,7 @@ export function EmailView({
|
||||
<div ref={threadComposeAnchorRef} className="mt-6 px-4 pb-6 pl-[68px]">
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-sm font-medium text-white"
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-sm font-bold text-white"
|
||||
style={{ backgroundColor: avatarColor(selfName) }}
|
||||
aria-hidden
|
||||
>
|
||||
|
||||
@ -141,7 +141,7 @@ export function Header({
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = 'none'
|
||||
target.parentElement!.innerHTML = `<div class="h-10 w-10 rounded-full bg-blue-500 flex items-center justify-center text-white font-medium">${app.name[0]}</div>`
|
||||
target.parentElement!.innerHTML = `<div class="h-10 w-10 rounded-full bg-blue-500 flex items-center justify-center text-white font-bold">${app.name[0]}</div>`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -154,7 +154,7 @@ export function Header({
|
||||
</div>
|
||||
|
||||
<Button variant="ghost" size="icon-lg" className="size-11 rounded-full overflow-hidden ml-2 p-0">
|
||||
<div className="h-10 w-10 rounded-full bg-purple-500 flex items-center justify-center text-white text-base font-medium">
|
||||
<div className="h-10 w-10 rounded-full bg-purple-500 flex items-center justify-center text-white text-base font-bold">
|
||||
E
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
@ -71,8 +71,14 @@ export function MailFolderStackIndicator({
|
||||
onNavigate,
|
||||
}: MailFolderStackIndicatorProps) {
|
||||
const items = useMemo(
|
||||
() => breadcrumbItemsForVisitKey(currentKey, folderTree, folderIdToLabel),
|
||||
[currentKey, folderTree, folderIdToLabel]
|
||||
() =>
|
||||
breadcrumbItemsForVisitKey(
|
||||
currentKey,
|
||||
folderTree,
|
||||
folderIdToLabel,
|
||||
labelRows
|
||||
),
|
||||
[currentKey, folderTree, folderIdToLabel, labelRows]
|
||||
)
|
||||
|
||||
const ariaLabel = items.map((i) => i.label).join(" · ")
|
||||
|
||||
72
components/gmail/mail-inbox-category-tab-icons.tsx
Normal file
72
components/gmail/mail-inbox-category-tab-icons.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
"use client"
|
||||
|
||||
import { memo } from "react"
|
||||
import { Icon } from "@iconify/react"
|
||||
import type { InboxCategoryTabIcon } from "@/lib/inbox-category-tabs"
|
||||
import { navFolderIconColorFromBgClass } from "@/lib/label-pill-contrast"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DEFAULT_ICON_CLASS = "size-4 shrink-0"
|
||||
|
||||
type MailInboxCategoryTabIconsProps = {
|
||||
tabs: readonly InboxCategoryTabIcon[]
|
||||
onTabClick?: (tabId: string) => void
|
||||
className?: string
|
||||
iconClassName?: string
|
||||
}
|
||||
|
||||
export const MailInboxCategoryTabIcons = memo(function MailInboxCategoryTabIcons({
|
||||
tabs,
|
||||
onTabClick,
|
||||
className,
|
||||
iconClassName = DEFAULT_ICON_CLASS,
|
||||
}: MailInboxCategoryTabIconsProps) {
|
||||
if (tabs.length === 0) return null
|
||||
|
||||
const ariaLabel = tabs.map((t) => t.label).join(", ")
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn("inline-flex shrink-0 items-center gap-0.5", className)}
|
||||
aria-label={`Catégories : ${ariaLabel}`}
|
||||
>
|
||||
{tabs.map((tab) => {
|
||||
const color = navFolderIconColorFromBgClass(tab.badgeColor)
|
||||
const icon = (
|
||||
<Icon
|
||||
icon={tab.icon}
|
||||
className={iconClassName}
|
||||
style={{ color }}
|
||||
aria-hidden
|
||||
/>
|
||||
)
|
||||
if (!onTabClick) {
|
||||
return (
|
||||
<span
|
||||
key={tab.id}
|
||||
title={tab.label}
|
||||
className="inline-flex items-center justify-center p-0.5"
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
title={tab.label}
|
||||
aria-label={tab.label}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onTabClick(tab.id)
|
||||
}}
|
||||
className="inline-flex cursor-pointer items-center justify-center rounded-full p-0.5 hover:bg-black/6"
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
352
components/gmail/mail-list-swipe-row.tsx
Normal file
352
components/gmail/mail-list-swipe-row.tsx
Normal file
@ -0,0 +1,352 @@
|
||||
"use client"
|
||||
|
||||
import { memo, useCallback, useRef, type ReactNode, type TouchEvent } from "react"
|
||||
import { Archive, Star, Tag, Trash2 } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const SNAP_MS = 200
|
||||
const SNAP_EASE = "cubic-bezier(0.25, 0.46, 0.45, 0.94)"
|
||||
const LEFT_PANEL_PX = 240
|
||||
const RIGHT_PANEL_PX = 100
|
||||
const OPEN_THRESHOLD_RATIO = 0.15
|
||||
const OPEN_VELOCITY = 0.28
|
||||
|
||||
type Props = {
|
||||
enabled: boolean
|
||||
emailId: string
|
||||
isOpen: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onArchive: () => void
|
||||
onDelete: () => void
|
||||
onStar: () => void
|
||||
onLabel: () => void
|
||||
className?: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
function MailListSwipeRowInner({
|
||||
enabled,
|
||||
emailId,
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
onArchive,
|
||||
onDelete,
|
||||
onStar,
|
||||
onLabel,
|
||||
className,
|
||||
children,
|
||||
}: Props) {
|
||||
const rowRef = useRef<HTMLDivElement>(null)
|
||||
const fgRef = useRef<HTMLDivElement>(null)
|
||||
const leftPanelRef = useRef<HTMLDivElement>(null)
|
||||
const rightPanelRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const offsetRef = useRef(0)
|
||||
const directionRef = useRef<"none" | "left" | "right">("none")
|
||||
const touchRef = useRef<{
|
||||
startX: number
|
||||
startY: number
|
||||
startOffset: number
|
||||
active: boolean
|
||||
axis: "none" | "x" | "y"
|
||||
startTime: number
|
||||
prevX: number
|
||||
prevT: number
|
||||
} | null>(null)
|
||||
const suppressClickUntilRef = useRef(0)
|
||||
|
||||
|
||||
const applyOffset = useCallback((px: number) => {
|
||||
offsetRef.current = px
|
||||
const fg = fgRef.current
|
||||
if (!fg) return
|
||||
fg.style.transform = `translate3d(${px}px,0,0)`
|
||||
|
||||
const lp = leftPanelRef.current
|
||||
const rp = rightPanelRef.current
|
||||
if (lp) lp.style.visibility = px > 0 ? "visible" : "hidden"
|
||||
if (rp) rp.style.visibility = px < 0 ? "visible" : "hidden"
|
||||
}, [])
|
||||
|
||||
const animateTo = useCallback(
|
||||
(px: number) => {
|
||||
const fg = fgRef.current
|
||||
if (!fg) return
|
||||
fg.style.transition = `transform ${SNAP_MS}ms ${SNAP_EASE}`
|
||||
applyOffset(px)
|
||||
window.setTimeout(() => {
|
||||
if (fg) fg.style.transition = "none"
|
||||
}, SNAP_MS + 10)
|
||||
},
|
||||
[applyOffset]
|
||||
)
|
||||
|
||||
const close = useCallback(() => {
|
||||
animateTo(0)
|
||||
directionRef.current = "none"
|
||||
if (isOpen) onOpenChange(false)
|
||||
}, [animateTo, isOpen, onOpenChange])
|
||||
|
||||
|
||||
const onTouchStart = useCallback(
|
||||
(e: TouchEvent<HTMLDivElement>) => {
|
||||
if (!enabled) return
|
||||
const t = e.touches[0]
|
||||
if (!t) return
|
||||
const fg = fgRef.current
|
||||
if (fg) fg.style.transition = "none"
|
||||
touchRef.current = {
|
||||
startX: t.clientX,
|
||||
startY: t.clientY,
|
||||
startOffset: offsetRef.current,
|
||||
active: true,
|
||||
axis: "none",
|
||||
startTime: performance.now(),
|
||||
prevX: t.clientX,
|
||||
prevT: performance.now(),
|
||||
}
|
||||
},
|
||||
[enabled]
|
||||
)
|
||||
|
||||
const onTouchMove = useCallback(
|
||||
(e: TouchEvent<HTMLDivElement>) => {
|
||||
const tr = touchRef.current
|
||||
if (!tr?.active) return
|
||||
const t = e.touches[0]
|
||||
if (!t) return
|
||||
|
||||
const dx = t.clientX - tr.startX
|
||||
const dy = t.clientY - tr.startY
|
||||
|
||||
if (tr.axis === "none") {
|
||||
if (Math.abs(dx) < 6 && Math.abs(dy) < 6) return
|
||||
if (Math.abs(dy) > Math.abs(dx)) {
|
||||
tr.axis = "y"
|
||||
tr.active = false
|
||||
return
|
||||
}
|
||||
tr.axis = "x"
|
||||
}
|
||||
|
||||
if (tr.axis !== "x") return
|
||||
e.preventDefault()
|
||||
|
||||
let raw = tr.startOffset + dx
|
||||
|
||||
// Clamp to panel width (no overswipe)
|
||||
if (raw > LEFT_PANEL_PX) raw = LEFT_PANEL_PX
|
||||
if (raw < -RIGHT_PANEL_PX) raw = -RIGHT_PANEL_PX
|
||||
|
||||
tr.prevX = t.clientX
|
||||
tr.prevT = performance.now()
|
||||
applyOffset(raw)
|
||||
},
|
||||
[applyOffset]
|
||||
)
|
||||
|
||||
const onTouchEnd = useCallback(() => {
|
||||
const tr = touchRef.current
|
||||
touchRef.current = null
|
||||
if (!tr?.active || tr.axis !== "x") return
|
||||
|
||||
suppressClickUntilRef.current = performance.now() + 350
|
||||
|
||||
const offset = offsetRef.current
|
||||
const elapsed = Math.max(1, performance.now() - tr.startTime)
|
||||
const instantVelocity = (tr.prevX - tr.startX) / elapsed
|
||||
|
||||
// If panel was open and user swiped opposite direction → just close
|
||||
if (directionRef.current === "right" && offset <= 0) {
|
||||
animateTo(0)
|
||||
directionRef.current = "none"
|
||||
if (isOpen) onOpenChange(false)
|
||||
return
|
||||
}
|
||||
if (directionRef.current === "left" && offset >= 0) {
|
||||
animateTo(0)
|
||||
directionRef.current = "none"
|
||||
if (isOpen) onOpenChange(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Open/close based on threshold + velocity
|
||||
// Closing requires less energy than opening (lower threshold & velocity)
|
||||
const closing = isOpen && (
|
||||
(directionRef.current === "right" && offset < LEFT_PANEL_PX) ||
|
||||
(directionRef.current === "left" && -offset < RIGHT_PANEL_PX)
|
||||
)
|
||||
const closeThreshold = 0.08
|
||||
const closeVelocity = 0.12
|
||||
|
||||
if (offset > 0) {
|
||||
if (closing) {
|
||||
const shouldStayOpen =
|
||||
offset >= LEFT_PANEL_PX * (1 - closeThreshold) &&
|
||||
instantVelocity >= -closeVelocity
|
||||
if (shouldStayOpen) {
|
||||
animateTo(LEFT_PANEL_PX)
|
||||
} else {
|
||||
animateTo(0)
|
||||
directionRef.current = "none"
|
||||
onOpenChange(false)
|
||||
}
|
||||
} else {
|
||||
const shouldOpen =
|
||||
offset >= LEFT_PANEL_PX * OPEN_THRESHOLD_RATIO ||
|
||||
(instantVelocity > OPEN_VELOCITY && offset > 20)
|
||||
if (shouldOpen) {
|
||||
animateTo(LEFT_PANEL_PX)
|
||||
directionRef.current = "right"
|
||||
if (!isOpen) onOpenChange(true)
|
||||
} else {
|
||||
animateTo(0)
|
||||
directionRef.current = "none"
|
||||
if (isOpen) onOpenChange(false)
|
||||
}
|
||||
}
|
||||
} else if (offset < 0) {
|
||||
if (closing) {
|
||||
const shouldStayOpen =
|
||||
-offset >= RIGHT_PANEL_PX * (1 - closeThreshold) &&
|
||||
instantVelocity <= closeVelocity
|
||||
if (shouldStayOpen) {
|
||||
animateTo(-RIGHT_PANEL_PX)
|
||||
} else {
|
||||
animateTo(0)
|
||||
directionRef.current = "none"
|
||||
onOpenChange(false)
|
||||
}
|
||||
} else {
|
||||
const shouldOpen =
|
||||
-offset >= RIGHT_PANEL_PX * OPEN_THRESHOLD_RATIO ||
|
||||
(instantVelocity < -OPEN_VELOCITY && offset < -20)
|
||||
if (shouldOpen) {
|
||||
animateTo(-RIGHT_PANEL_PX)
|
||||
directionRef.current = "left"
|
||||
if (!isOpen) onOpenChange(true)
|
||||
} else {
|
||||
animateTo(0)
|
||||
directionRef.current = "none"
|
||||
if (isOpen) onOpenChange(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
directionRef.current = "none"
|
||||
if (isOpen) onOpenChange(false)
|
||||
}
|
||||
}, [
|
||||
animateTo,
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
])
|
||||
|
||||
const onTouchCancel = useCallback(() => {
|
||||
const tr = touchRef.current
|
||||
touchRef.current = null
|
||||
if (!tr?.active || tr.axis !== "x") return
|
||||
animateTo(0)
|
||||
directionRef.current = "none"
|
||||
if (isOpen) onOpenChange(false)
|
||||
}, [animateTo, isOpen, onOpenChange])
|
||||
|
||||
const handleClickCapture = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (!enabled) return
|
||||
if (performance.now() < suppressClickUntilRef.current) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
if (offsetRef.current !== 0) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
close()
|
||||
}
|
||||
},
|
||||
[close, enabled]
|
||||
)
|
||||
|
||||
// Close when isOpen flips to false externally (another row opened)
|
||||
if (!isOpen && offsetRef.current !== 0 && !touchRef.current?.active) {
|
||||
const fg = fgRef.current
|
||||
if (fg) {
|
||||
fg.style.transition = `transform ${SNAP_MS}ms ${SNAP_EASE}`
|
||||
applyOffset(0)
|
||||
directionRef.current = "none"
|
||||
window.setTimeout(() => {
|
||||
if (fg) fg.style.transition = "none"
|
||||
}, SNAP_MS + 10)
|
||||
}
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
return <div className={className}>{children}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={rowRef} className={cn("relative overflow-hidden", className)} data-swipe-row-id={emailId}>
|
||||
{/* Left actions (swipe right to reveal) */}
|
||||
<div className="invisible absolute inset-y-0 left-0 flex" ref={leftPanelRef}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Mettre en suivi"
|
||||
className="flex h-full w-20 shrink-0 flex-col items-center justify-center gap-1 bg-[#f4b400] px-1 text-[11px] font-medium text-white"
|
||||
onClick={(e) => { e.stopPropagation(); onStar(); close() }}
|
||||
>
|
||||
<Star className="size-5 fill-white text-white" strokeWidth={0} />
|
||||
<span className="max-w-full truncate text-center">Suivi</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Ajouter un libellé"
|
||||
className="flex h-full w-20 shrink-0 flex-col items-center justify-center gap-1 bg-[#34a853] px-1 text-[11px] font-medium text-white"
|
||||
onClick={(e) => { e.stopPropagation(); onLabel(); close() }}
|
||||
>
|
||||
<Tag className="size-5 text-white" strokeWidth={1.75} />
|
||||
<span className="max-w-full truncate text-center">Libellé</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Archiver"
|
||||
className="flex h-full w-20 shrink-0 flex-col items-center justify-center gap-1 bg-[#1a73e8] px-1 text-[11px] font-medium text-white"
|
||||
onClick={(e) => { e.stopPropagation(); onArchive(); close() }}
|
||||
>
|
||||
<Archive className="size-5 text-white" strokeWidth={1.75} />
|
||||
<span className="max-w-full truncate text-center">Archiver</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Right actions (swipe left to reveal) */}
|
||||
<div className="invisible absolute inset-y-0 right-0 flex" ref={rightPanelRef}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Supprimer"
|
||||
className="flex h-full w-[100px] shrink-0 flex-col items-center justify-center gap-1 bg-[#d93025] px-2 text-[11px] font-medium text-white"
|
||||
onClick={(e) => { e.stopPropagation(); onDelete(); close() }}
|
||||
>
|
||||
<Trash2 className="size-5 text-white" strokeWidth={1.75} />
|
||||
<span className="max-w-full truncate text-center">Supprimer</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Foreground (row content) */}
|
||||
<div
|
||||
ref={fgRef}
|
||||
className="relative z-1 bg-inherit will-change-transform"
|
||||
style={{ transform: "translate3d(0,0,0)" }}
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchMove={onTouchMove}
|
||||
onTouchEnd={onTouchEnd}
|
||||
onTouchCancel={onTouchCancel}
|
||||
onClickCapture={handleClickCapture}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const MailListSwipeRow = memo(MailListSwipeRowInner)
|
||||
@ -1005,7 +1005,9 @@ export function Sidebar({
|
||||
|
||||
/** pl-6 + demi-largeur icône nav (h-5) → axe à 34px ; picto split (size-9) centré sur cet axe. */
|
||||
const splitViewLogoIconClass = "size-9 shrink-0"
|
||||
const splitViewLogoHeaderClass = "min-h-10 pl-4 pr-3.5 pb-2"
|
||||
/** Aligné sur la barre split (pt-1 shell + py-2 + recherche h-12) et le bouton menu size-9. */
|
||||
const splitViewLogoHeaderClass =
|
||||
"box-border min-h-[80px] pt-3 pl-4 pr-3.5 pb-4"
|
||||
|
||||
/** Same row geometry collapsed / expanded / hover so icons never jump (h-8, pl-6 icon column). */
|
||||
const NavItem = ({
|
||||
@ -2687,7 +2689,11 @@ export function Sidebar({
|
||||
className="sticky top-0 z-30 flex h-8 w-full min-w-0 shrink-0 items-center gap-2 bg-app-canvas pl-6 pr-3"
|
||||
title={!isExpanded ? "Libellés" : undefined}
|
||||
>
|
||||
<Tag className="h-5 w-5 shrink-0 text-gray-600" aria-hidden />
|
||||
<Icon
|
||||
icon="mdi:label-outline"
|
||||
className="h-5 w-5 shrink-0 text-gray-600"
|
||||
aria-hidden
|
||||
/>
|
||||
{isExpanded && (
|
||||
<span className="min-w-0 flex-1 truncate text-left text-sm font-medium text-gray-700">
|
||||
Libellés
|
||||
|
||||
@ -22,4 +22,4 @@ export function folderTreeNavIconNameClosed(hasChildren: boolean): string {
|
||||
return folderTreeNavIconName(hasChildren, false)
|
||||
}
|
||||
|
||||
export const FOLDER_SECTION_ICON = "fluent:folder-mail-20-filled"
|
||||
export const FOLDER_SECTION_ICON = "fluent:folder-mail-20-regular"
|
||||
|
||||
59
lib/inbox-category-tabs.ts
Normal file
59
lib/inbox-category-tabs.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import type { Email } from "@/lib/email-data"
|
||||
import {
|
||||
emailMatchesFolder,
|
||||
type MailFolderFilterCtx,
|
||||
type MailNavFolderMaps,
|
||||
} from "@/lib/mail-folder-filter"
|
||||
import { DEFAULT_INBOX_TAB, INBOX_ALL_TAB } from "@/lib/mail-url"
|
||||
import {
|
||||
tabbedInboxLabelRows,
|
||||
type LabelRowItem,
|
||||
} from "@/lib/sidebar-nav-data"
|
||||
|
||||
export type InboxCategoryTabIcon = {
|
||||
id: string
|
||||
label: string
|
||||
icon: string
|
||||
badgeColor: string
|
||||
}
|
||||
|
||||
/** Onglets catégorie boîte (Principale + libellés tabbed), hors « Tous les messages ». */
|
||||
export function buildInboxCategoryTabIcons(
|
||||
labelRows: readonly LabelRowItem[]
|
||||
): InboxCategoryTabIcon[] {
|
||||
return [
|
||||
{
|
||||
id: DEFAULT_INBOX_TAB,
|
||||
label: "Principale",
|
||||
icon: "mdi:account",
|
||||
badgeColor: "bg-[#0b57d0]",
|
||||
},
|
||||
...tabbedInboxLabelRows(labelRows).map((r) => ({
|
||||
id: r.id,
|
||||
label: r.label,
|
||||
icon: r.icon ?? "mdi:label-outline",
|
||||
badgeColor: r.color,
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
/** Onglets catégorie (hors Principale) auxquels le message appartient — pour la liste « Tous les messages ». */
|
||||
export function resolveEmailInboxCategoryTabs(
|
||||
email: Email,
|
||||
ctx: MailFolderFilterCtx,
|
||||
maps: MailNavFolderMaps,
|
||||
tabs: readonly InboxCategoryTabIcon[],
|
||||
subtreeIdsCache?: Map<string, string[] | null>
|
||||
): InboxCategoryTabIcon[] {
|
||||
const matched: InboxCategoryTabIcon[] = []
|
||||
for (const tab of tabs) {
|
||||
if (tab.id === INBOX_ALL_TAB || tab.id === DEFAULT_INBOX_TAB) continue
|
||||
if (
|
||||
emailMatchesFolder(email, "inbox", ctx, maps, subtreeIdsCache) &&
|
||||
emailMatchesFolder(email, tab.id, ctx, maps, subtreeIdsCache)
|
||||
) {
|
||||
matched.push(tab)
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
@ -1,10 +1,16 @@
|
||||
import type { FolderTreeNode } from "@/lib/sidebar-nav-maps"
|
||||
import { findFolderPath } from "@/lib/sidebar-nav-folder-ids"
|
||||
import { getMailNavFolderLabel } from "@/lib/sidebar-nav-data"
|
||||
import {
|
||||
defaultNavLabelRowsSnapshot,
|
||||
getMailNavFolderLabel,
|
||||
inboxTabDisplayLabel,
|
||||
type LabelRowItem,
|
||||
} from "@/lib/sidebar-nav-data"
|
||||
|
||||
/** @deprecated Utiliser `getMailNavFolderLabel(inboxTab, folderIdToLabel)` ou `inboxTabDisplayLabel`. */
|
||||
export const INBOX_CATEGORY_TAB_LABELS: Record<string, string> = {
|
||||
primary: "Principale",
|
||||
all: "Tous les messages",
|
||||
}
|
||||
|
||||
/** Clé stable pour historique navigation (dossier + onglet boîte de réception). */
|
||||
@ -88,14 +94,19 @@ export function getMailNavFolderBreadcrumbSegments(
|
||||
export function breadcrumbItemsForVisitKey(
|
||||
key: string,
|
||||
folderTree: FolderTreeNode[],
|
||||
folderIdToLabel?: Record<string, string>
|
||||
folderIdToLabel?: Record<string, string>,
|
||||
labelRows: readonly LabelRowItem[] = defaultNavLabelRowsSnapshot
|
||||
): MailNavBreadcrumbItem[] {
|
||||
const { folderId, inboxTab } = parseMailNavVisitKey(key)
|
||||
const inboxCategory =
|
||||
folderId === "inbox" && inboxTab && inboxTab !== "primary"
|
||||
? {
|
||||
tabId: inboxTab,
|
||||
label: getMailNavFolderLabel(inboxTab, folderIdToLabel),
|
||||
label: inboxTabDisplayLabel(
|
||||
inboxTab,
|
||||
labelRows,
|
||||
folderIdToLabel ?? {}
|
||||
),
|
||||
}
|
||||
: null
|
||||
return getMailNavFolderBreadcrumbItems(
|
||||
@ -109,19 +120,27 @@ export function breadcrumbItemsForVisitKey(
|
||||
export function breadcrumbSegmentsForVisitKey(
|
||||
key: string,
|
||||
folderTree: FolderTreeNode[],
|
||||
folderIdToLabel?: Record<string, string>
|
||||
folderIdToLabel?: Record<string, string>,
|
||||
labelRows?: readonly LabelRowItem[]
|
||||
): string[] {
|
||||
return breadcrumbItemsForVisitKey(key, folderTree, folderIdToLabel).map(
|
||||
(i) => i.label
|
||||
)
|
||||
return breadcrumbItemsForVisitKey(
|
||||
key,
|
||||
folderTree,
|
||||
folderIdToLabel,
|
||||
labelRows
|
||||
).map((i) => i.label)
|
||||
}
|
||||
|
||||
export function breadcrumbForVisitKey(
|
||||
key: string,
|
||||
folderTree: FolderTreeNode[],
|
||||
folderIdToLabel?: Record<string, string>
|
||||
folderIdToLabel?: Record<string, string>,
|
||||
labelRows?: readonly LabelRowItem[]
|
||||
): string {
|
||||
return breadcrumbSegmentsForVisitKey(key, folderTree, folderIdToLabel).join(
|
||||
" · "
|
||||
)
|
||||
return breadcrumbSegmentsForVisitKey(
|
||||
key,
|
||||
folderTree,
|
||||
folderIdToLabel,
|
||||
labelRows
|
||||
).join(" · ")
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
ClockArrowUp,
|
||||
ShieldAlert,
|
||||
Trash2,
|
||||
User,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
folderTreeNavIconNameClosed,
|
||||
@ -47,7 +48,8 @@ export function resolveMailNavIcon(
|
||||
|
||||
if (folderId === "inbox") {
|
||||
const tab = normalizeInboxTabSegment(inboxTab ?? "primary")
|
||||
if (tab === "primary") return { kind: "lucide", Icon: Inbox }
|
||||
if (tab === "primary") return { kind: "lucide", Icon: User }
|
||||
if (tab === "all") return { kind: "lucide", Icon: Inbox }
|
||||
const row = labelRowById(labelRows, tab)
|
||||
if (row?.icon) return { kind: "iconify", icon: row.icon }
|
||||
return { kind: "lucide", Icon: Inbox }
|
||||
|
||||
@ -7,6 +7,14 @@ import {
|
||||
|
||||
export const DEFAULT_MAIL_FOLDER = "inbox"
|
||||
export const DEFAULT_INBOX_TAB = "primary"
|
||||
/** Onglet boîte : tous les messages inbox sans filtre libellé catégorie. */
|
||||
export const INBOX_ALL_TAB = "all"
|
||||
|
||||
/** Onglets sans pastille « nouveaux » ni ligne expéditeurs quand inactifs. */
|
||||
export function inboxTabShowsInactiveMeta(tabId: string): boolean {
|
||||
const tab = normalizeInboxTabSegment(tabId)
|
||||
return tab !== DEFAULT_INBOX_TAB && tab !== INBOX_ALL_TAB
|
||||
}
|
||||
|
||||
/** Segments d’URL historiques → ids libellés actuels. */
|
||||
const LEGACY_INBOX_TAB_SEGMENT: Record<string, string> = {
|
||||
@ -22,7 +30,7 @@ export function normalizeInboxTabSegment(tab: string): string {
|
||||
}
|
||||
|
||||
function defaultInboxTabIdSet(): Set<string> {
|
||||
const s = new Set<string>([DEFAULT_INBOX_TAB])
|
||||
const s = new Set<string>([DEFAULT_INBOX_TAB, INBOX_ALL_TAB])
|
||||
for (const r of tabbedInboxLabelRows(cloneDefaultLabelRows())) {
|
||||
s.add(r.id)
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ export const SYSTEM_NAV_LABEL_DEFAULTS: readonly LabelRowItem[] = [
|
||||
id: "promotions",
|
||||
label: "Promotions",
|
||||
color: "bg-[#1e8e3e]",
|
||||
icon: "mdi:tag-outline",
|
||||
icon: "mdi:tag",
|
||||
tabbed: true,
|
||||
favorite: true,
|
||||
excludeFromPrincipal: true,
|
||||
@ -230,6 +230,7 @@ export function inboxTabDisplayLabel(
|
||||
folderIdToLabel: Record<string, string>
|
||||
): string {
|
||||
if (inboxTab === "primary") return "Principale"
|
||||
if (inboxTab === "all") return "Tous les messages"
|
||||
const row = labelRowById(labelRows, inboxTab)
|
||||
if (row && row.enabled !== false) return row.label
|
||||
return getMailNavFolderLabel(inboxTab, folderIdToLabel)
|
||||
@ -265,7 +266,11 @@ export function reconcileLabelRowsFromPersisted(persisted: LabelRowItem[] | unde
|
||||
const merged = defaults.map((d) => {
|
||||
const p = persistByResolvedId.get(d.id)
|
||||
if (!p) return { ...d }
|
||||
return normalizeLabelRow({ ...d, ...p, id: d.id })
|
||||
const row = normalizeLabelRow({ ...d, ...p, id: d.id })
|
||||
if (SYSTEM_NAV_LABEL_ID_SET.has(d.id)) {
|
||||
return { ...row, icon: d.icon, color: d.color }
|
||||
}
|
||||
return row
|
||||
})
|
||||
|
||||
const mergedIds = new Set(merged.map((r) => r.id))
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user