ultisuite-client/components/gmail/compact-inbox-category-tabs.tsx

175 lines
5.5 KiB
TypeScript

"use client"
import { memo, useEffect, useLayoutEffect, useRef, useState } from "react"
import { Icon } from "@iconify/react"
import { inboxTabActiveAccentColor } from "@/lib/inbox-category-tabs"
import { inboxTabShowsInactiveMeta } from "@/lib/mail-url"
import { cn } from "@/lib/utils"
export type CompactInboxCategoryTab = {
id: string
label: string
icon: string
badgeColor: string
}
const TAB_ICON_CLASS = "h-4 w-4 shrink-0"
function inboxTabBadgeDotClass(badgeColor: string) {
return cn(
"absolute -right-0.5 -top-0.5 size-2 rounded-full ring-2 ring-white",
badgeColor
)
}
type CompactInboxCategoryTabsProps = {
tabs: readonly CompactInboxCategoryTab[]
activeTabId: string
unseenInTabById: Record<string, number>
onTabClick: (tabId: string) => void
}
export const CompactInboxCategoryTabs = memo(function CompactInboxCategoryTabs({
tabs,
activeTabId,
unseenInTabById,
onTabClick,
}: CompactInboxCategoryTabsProps) {
const barRef = useRef<HTMLDivElement>(null)
const [indicator, setIndicator] = useState({ x: 0, width: 0, ready: false })
const [optimisticTabId, setOptimisticTabId] = useState<string | null>(null)
const displayedTabId = optimisticTabId ?? activeTabId
useEffect(() => {
setOptimisticTabId(null)
}, [activeTabId])
useLayoutEffect(() => {
const bar = barRef.current
if (!bar) return
let raf = 0
const measure = () => {
cancelAnimationFrame(raf)
raf = requestAnimationFrame(() => {
const activeBtn = bar.querySelector<HTMLButtonElement>(
`[data-inbox-tab="${displayedTabId}"]`
)
if (!activeBtn) return
const barRect = bar.getBoundingClientRect()
const btnRect = activeBtn.getBoundingClientRect()
const x = btnRect.left - barRect.left
const width = btnRect.width
setIndicator((prev) =>
prev.ready && prev.x === x && prev.width === width
? prev
: { x, width, ready: true }
)
})
}
measure()
const onResize = () => measure()
window.addEventListener("resize", onResize, { passive: true })
const ro =
typeof ResizeObserver !== "undefined"
? new ResizeObserver(onResize)
: null
ro?.observe(bar)
return () => {
cancelAnimationFrame(raf)
window.removeEventListener("resize", onResize)
ro?.disconnect()
}
}, [displayedTabId, tabs])
const activeTab = tabs.find((t) => t.id === displayedTabId)
const activeAccentColor = activeTab
? inboxTabActiveAccentColor(activeTab.id, activeTab.badgeColor)
: "#0b57d0"
return (
<div ref={barRef} className="relative flex w-full min-w-0">
<span
aria-hidden
className={cn(
"pointer-events-none absolute bottom-0 left-0 z-[2] h-[3px] origin-left rounded-t-sm",
"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"
)}
style={{
width: indicator.width,
transform: `translate3d(${indicator.x}px, 0, 0)`,
backgroundColor: activeAccentColor,
}}
/>
{tabs.map((tab) => {
const isActive = displayedTabId === tab.id
const accentColor = isActive
? inboxTabActiveAccentColor(tab.id, tab.badgeColor)
: undefined
const unseen = unseenInTabById[tab.id] ?? 0
const showMeta =
inboxTabShowsInactiveMeta(tab.id) && !isActive && unseen > 0
return (
<button
key={tab.id}
type="button"
data-inbox-tab={tab.id}
title={!isActive ? tab.label : undefined}
aria-label={tab.label}
aria-current={isActive ? "true" : undefined}
onClick={() => {
if (tab.id !== activeTabId) setOptimisticTabId(tab.id)
onTabClick(tab.id)
}}
className={cn(
"relative z-[1] flex min-h-10 cursor-pointer items-center justify-center px-1",
"transition-colors duration-200 motion-reduce:transition-none",
isActive ? "shrink-0 flex-none" : "min-w-0 flex-1 overflow-hidden",
!isActive && "hover:bg-[#f1f3f4]"
)}
>
<div
className={cn(
"flex h-10 items-center justify-center gap-1.5",
isActive ? "shrink-0 px-0.5" : "w-full min-w-0"
)}
>
<div className="relative inline-flex shrink-0">
<Icon
icon={tab.icon}
className={cn(
TAB_ICON_CLASS,
"transition-colors duration-200 motion-reduce:transition-none",
!isActive && "text-[#5f6368]"
)}
style={accentColor ? { color: accentColor } : undefined}
aria-hidden
/>
{showMeta && unseen > 0 ? (
<span
className={inboxTabBadgeDotClass(tab.badgeColor)}
aria-hidden
/>
) : null}
</div>
{isActive ? (
<span
className="shrink-0 whitespace-nowrap text-[13px] font-semibold leading-tight"
style={{ color: accentColor }}
>
{tab.label}
</span>
) : null}
</div>
</button>
)
})}
</div>
)
})