ultisuite-client/components/gmail/mail-label-pills.tsx
2026-05-16 20:30:50 +02:00

290 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useMemo, type MouseEvent } from "react"
import { cn } from "@/lib/utils"
import {
labelPillTextClassForBgHex,
labelPillTextClassForTailwindBgUtility,
} from "@/lib/label-pill-contrast"
import type { FolderTreeNode, LabelRowItem } from "@/lib/sidebar-nav-data"
/** Libellés système masqués en pastilles liste (Gmail : pastilles = libellés perso / dossiers). */
export const MAIL_LABEL_STRIP_EXCLUDE = new Set([
"inbox",
"sent",
"drafts",
"spam",
"starred",
"snoozed",
"trash",
"important",
"scheduled",
])
export function buildLabelTextToNavColorClass(
tree: FolderTreeNode[],
labelRows: LabelRowItem[]
): Map<string, string> {
const m = new Map<string, string>()
const put = (text: string, color: string | undefined) => {
if (!color) return
m.set(text.toLowerCase(), color)
}
const walk = (nodes: FolderTreeNode[]) => {
for (const n of nodes) {
put(n.label, n.color)
if (n.children?.length) walk(n.children)
}
}
walk(tree)
for (const r of labelRows) put(r.label, r.color)
return m
}
export function mailLabelShouldShowInListStrip(
lab: string,
emailLabelToSidebarFolderId: Record<string, string>,
getNavItemPrefs: (id: string) => { messages: string },
labelRows?: readonly LabelRowItem[]
): boolean {
if (MAIL_LABEL_STRIP_EXCLUDE.has(lab.toLowerCase())) return false
const fid = emailLabelToSidebarFolderId[lab]
let row: LabelRowItem | undefined
if (fid) row = labelRows?.find((r) => r.id === fid)
if (!row && labelRows?.length) {
row = labelRows.find((r) => r.label.toLowerCase() === lab.toLowerCase())
}
if (row) {
if (row.enabled === false) return false
if (row.showInMessageList === false) return false
}
if (fid) return getNavItemPrefs(fid).messages !== "hide"
return true
}
function findNodeLabel(tree: FolderTreeNode[], folderId: string): string | null {
for (const n of tree) {
if (n.id === folderId) return n.label
if (n.children?.length) {
const hit = findNodeLabel(n.children, folderId)
if (hit) return hit
}
}
return null
}
function collectFolderLabelsSet(tree: FolderTreeNode[]): Set<string> {
const s = new Set<string>()
const walk = (nodes: FolderTreeNode[]) => {
for (const n of nodes) {
s.add(n.label.toLowerCase())
if (n.children?.length) walk(n.children)
}
}
walk(tree)
return s
}
const PILL_BASE =
"shrink-0 whitespace-nowrap rounded font-semibold leading-snug opacity-[0.96] transition-[filter,opacity] duration-150 hover:opacity-100 hover:brightness-[0.98]"
function MailLabelPill({
label,
displayText,
bgClass,
size,
onClick,
onRemoveClick,
}: {
label: string
displayText: string
bgClass: string | undefined
size: "list" | "header"
onClick: (e: MouseEvent) => void
onRemoveClick?: (e: MouseEvent) => void
}) {
const sizeClass =
size === "list"
? "px-1 py-px text-[11px]"
: "inline-flex items-center gap-0.5 px-1.5 py-0.5 text-xs"
return (
<button
type="button"
title={label}
className={cn(
PILL_BASE,
sizeClass,
bgClass
? cn(bgClass, labelPillTextClassForTailwindBgUtility(bgClass))
: cn(
"border border-transparent bg-[#e8eaed] hover:bg-[#dadce0]",
labelPillTextClassForBgHex("#e8eaed")
)
)}
onClick={onClick}
>
{displayText}
{onRemoveClick ? (
<span
role="button"
tabIndex={0}
className={cn(
"text-inherit opacity-70 hover:opacity-100",
size === "header" ? "ml-0.5" : "ml-0.5"
)}
aria-label={`Retirer le libellé ${displayText}`}
onClick={onRemoveClick}
onKeyDown={(e) => {
if (e.key === "Enter") onRemoveClick(e as unknown as MouseEvent)
}}
>
×
</span>
) : null}
</button>
)
}
type MailLabelPillStripProps = {
labels: string[] | undefined
labelBgByText: Map<string, string>
emailLabelToSidebarFolderId: Record<string, string>
getNavItemPrefs: (id: string) => { messages: string }
onLabelNavigate: (label: string) => void
variant: "list" | "header"
/** En-tête message : filtre additionnel (ex. libellés système avec libellé lisible). */
showLabel?: (label: string) => boolean
/** En-tête : nom affiché (Boîte de réception, etc.). */
resolveDisplayName?: (label: string) => string
/** En-tête : pastille spam avec action. */
spamChip?: { onNotSpam: () => void }
/** En-tête : bouton × sur chaque pastille. */
showRemoveOnPills?: boolean
/** Dossier actuel (id) — son label est masqué en list, mais pas ses sous-dossiers. */
currentFolderId?: string
/** Libellés navigation (couleurs / enabled / showInMessageList). */
labelRows?: readonly LabelRowItem[]
/** Arbre dossiers (tri pastilles liste, masquage libellé du dossier courant). */
folderTree?: FolderTreeNode[]
}
export function MailLabelPillStrip({
labels,
labelBgByText,
emailLabelToSidebarFolderId,
getNavItemPrefs,
onLabelNavigate,
variant,
showLabel,
resolveDisplayName,
spamChip,
showRemoveOnPills,
currentFolderId,
folderTree,
labelRows,
}: MailLabelPillStripProps) {
const currentFolderLabel = useMemo(() => {
if (!currentFolderId || !folderTree) return null
return findNodeLabel(folderTree, currentFolderId)
}, [currentFolderId, folderTree])
const folderLabelsLower = useMemo(() => {
if (!folderTree) return new Set<string>()
return collectFolderLabelsSet(folderTree)
}, [folderTree])
const shown = useMemo(() => {
const filtered = (labels ?? []).filter((lab) => {
if (variant === "list") {
if (
!mailLabelShouldShowInListStrip(
lab,
emailLabelToSidebarFolderId,
getNavItemPrefs,
labelRows
)
) {
return false
}
if (currentFolderLabel && lab.toLowerCase() === currentFolderLabel.toLowerCase()) {
return false
}
return true
}
if (!mailLabelShouldShowInListStrip(lab, emailLabelToSidebarFolderId, getNavItemPrefs, labelRows)) {
return false
}
if (lab === "spam" && spamChip) return false
if (showLabel && !showLabel(lab)) return false
return true
})
if (variant === "list" && folderTree) {
filtered.sort((a, b) => {
const aIsFolder = folderLabelsLower.has(a.toLowerCase())
const bIsFolder = folderLabelsLower.has(b.toLowerCase())
if (aIsFolder && !bIsFolder) return -1
if (!aIsFolder && bIsFolder) return 1
return 0
})
}
return filtered
}, [labels, variant, emailLabelToSidebarFolderId, getNavItemPrefs, labelRows, currentFolderLabel, folderTree, folderLabelsLower, spamChip, showLabel])
const hasSpam = variant === "header" && spamChip
if (shown.length === 0 && !hasSpam) return null
const wrapClass =
variant === "list"
? "flex shrink-0 items-center gap-1"
: "contents"
return (
<span className={wrapClass}>
{hasSpam ? (
<button
type="button"
onClick={() => spamChip!.onNotSpam()}
className={cn(
PILL_BASE,
"inline-flex items-center gap-0.5 border border-transparent bg-[#e8eaed] px-1.5 py-0.5 text-xs hover:bg-[#dadce0]",
labelPillTextClassForBgHex("#e8eaed")
)}
title="Signaler comme non-spam et retirer ce libellé"
>
Spam
<span className="opacity-70 hover:opacity-100" aria-hidden>
×
</span>
</button>
) : null}
{shown.map((lab) => {
const bg = labelBgByText.get(lab.toLowerCase())
const displayText = resolveDisplayName?.(lab) ?? lab
return (
<MailLabelPill
key={lab}
label={lab}
displayText={displayText}
bgClass={bg}
size={variant === "list" ? "list" : "header"}
onClick={(e) => {
e.stopPropagation()
onLabelNavigate(lab)
}}
onRemoveClick={
showRemoveOnPills
? (e) => {
e.stopPropagation()
}
: undefined
}
/>
)
})}
</span>
)
}