267 lines
7.5 KiB
TypeScript
267 lines
7.5 KiB
TypeScript
"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 }
|
||
): boolean {
|
||
if (MAIL_LABEL_STRIP_EXCLUDE.has(lab.toLowerCase())) return false
|
||
const fid = emailLabelToSidebarFolderId[lab]
|
||
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
|
||
/** Arbre dossiers — nécessaire pour déterminer le label du dossier courant et l'ordre. */
|
||
folderTree?: FolderTreeNode[]
|
||
}
|
||
|
||
export function MailLabelPillStrip({
|
||
labels,
|
||
labelBgByText,
|
||
emailLabelToSidebarFolderId,
|
||
getNavItemPrefs,
|
||
onLabelNavigate,
|
||
variant,
|
||
showLabel,
|
||
resolveDisplayName,
|
||
spamChip,
|
||
showRemoveOnPills,
|
||
currentFolderId,
|
||
folderTree,
|
||
}: 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)) {
|
||
return false
|
||
}
|
||
if (currentFolderLabel && lab.toLowerCase() === currentFolderLabel.toLowerCase()) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
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, 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>
|
||
)
|
||
}
|