ultisuite-client/components/gmail/sidebar/sidebar-nav-primitives.tsx
2026-05-20 16:01:08 +02:00

295 lines
7.5 KiB
TypeScript

"use client"
import {
useState,
type ReactNode,
type CSSProperties,
type KeyboardEvent,
type DragEvent,
} from "react"
import { Check, GripVertical } from "lucide-react"
import { Icon } from "@iconify/react"
import { cn, formatCount } from "@/lib/utils"
import {
MAIL_SIDEBAR_MENU_ITEM_CLASS,
MAIL_SIDEBAR_OVERFLOW_BTN_CLASS,
} from "@/lib/mail-chrome-classes"
import {
DropdownMenuItem,
} from "@/components/ui/dropdown-menu"
import {
ContextMenuItem,
} from "@/components/ui/context-menu"
import type { FolderTreeNode } from "@/lib/sidebar-nav-data"
import {
folderTreeNavIconName,
navFolderIconColorFromBgClass,
} from "@/lib/folder-nav-icons"
import type { SidebarNavDropPlacement } from "@/lib/sidebar-nav-dnd"
export function LabelMenuOptionWithCheck({
checked,
onPick,
children,
}: {
checked: boolean
onPick: () => void
children: ReactNode
}) {
return (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
onPick()
}}
className={MAIL_SIDEBAR_MENU_ITEM_CLASS}
>
<span className="min-w-0 flex-1 text-left">{children}</span>
<span
className="flex size-4 shrink-0 items-center justify-center"
aria-hidden={!checked}
>
{checked ? (
<Check className="size-4 text-foreground" strokeWidth={2} aria-hidden />
) : null}
</span>
</DropdownMenuItem>
)
}
export function ContextLabelMenuOptionWithCheck({
checked,
onPick,
children,
}: {
checked: boolean
onPick: () => void
children: ReactNode
}) {
return (
<ContextMenuItem
onClick={() => onPick()}
className={MAIL_SIDEBAR_MENU_ITEM_CLASS}
>
<span className="min-w-0 flex-1 text-left">{children}</span>
<span
className="flex size-4 shrink-0 items-center justify-center"
aria-hidden={!checked}
>
{checked ? (
<Check className="size-4 text-foreground" strokeWidth={2} aria-hidden />
) : null}
</span>
</ContextMenuItem>
)
}
export function folderParentSelectOptions(tree: FolderTreeNode[]): {
value: string
label: string
}[] {
const out: { value: string; label: string }[] = [
{ value: "__root__", label: "Racine" },
]
const walk = (nodes: FolderTreeNode[], depth: number) => {
for (const n of nodes) {
out.push({
value: n.id,
label: `${"\u2003".repeat(depth * 2)}${n.label}`,
})
if (n.children?.length) walk(n.children, depth + 1)
}
}
walk(tree, 0)
return out
}
export function navRowRoundedWhenActive(active: boolean) {
return active ? "rounded-r-full" : "rounded-r-none hover:rounded-r-full"
}
export function SidebarNavIconUnreadDot({ show }: { show: boolean }) {
if (!show) return null
return (
<>
<span
className="pointer-events-none absolute -right-0.5 -top-0.5 size-2 rounded-full bg-yellow-400 opacity-75 motion-reduce:animate-none animate-ping"
aria-hidden
/>
<span
className="pointer-events-none absolute -right-0.5 -top-0.5 size-2 rounded-full bg-yellow-400"
aria-hidden
/>
</>
)
}
export function SidebarNavIconSlot({
showUnreadDot,
children,
}: {
showUnreadDot?: boolean
children: ReactNode
}) {
return (
<span className="relative flex h-5 w-5 shrink-0 items-center justify-center">
{children}
<SidebarNavIconUnreadDot show={!!showUnreadDot} />
</span>
)
}
export function markNavDragSource(el: HTMLElement | null) {
el?.setAttribute("data-nav-drag-source", "true")
}
export function unmarkNavDragSource(el: HTMLElement | null) {
el?.removeAttribute("data-nav-drag-source")
}
export function setNavDropIndicator(
el: HTMLElement | null,
placement: SidebarNavDropPlacement | null
) {
if (!el) return
if (placement) {
el.setAttribute("data-nav-drop", placement)
} else {
el.removeAttribute("data-nav-drop")
}
}
export function navRowActivate(e: KeyboardEvent, action: () => void) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
action()
}
}
export function FolderTreeNavIcon({
hasChildren,
open,
colorBgClass,
className,
style,
}: {
hasChildren: boolean
open: boolean
colorBgClass: string
className?: string
style?: CSSProperties
}) {
return (
<Icon
icon={folderTreeNavIconName(hasChildren, open)}
className={cn("h-5 w-5 shrink-0", className)}
style={{ color: navFolderIconColorFromBgClass(colorBgClass), ...style }}
aria-hidden
/>
)
}
export function SidebarNavDragHandle({
label,
onDragStart,
onDragEnd,
}: {
label: string
onDragStart: (e: DragEvent<HTMLSpanElement>) => void
onDragEnd: () => void
}) {
return (
<span
draggable
title={`Réorganiser : ${label}`}
aria-label={`Réorganiser : ${label}`}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
className="pointer-events-none absolute left-0 top-1/2 z-10 flex h-8 w-4 -translate-y-1/2 cursor-grab items-center justify-center text-gray-400 opacity-0 transition-opacity hover:opacity-100 active:cursor-grabbing group-hover/folderrow:pointer-events-auto group-hover/folderrow:opacity-100 group-hover/labelrow:pointer-events-auto group-hover/labelrow:opacity-100"
>
<GripVertical className="h-3.5 w-3.5" aria-hidden />
</span>
)
}
const OVERFLOW_COUNT_HOVER_HIDE = {
folderrow: "group-hover/folderrow:opacity-0",
labelrow: "group-hover/labelrow:opacity-0",
catnav: "group-hover/catnav:opacity-0",
} as const
const OVERFLOW_MENU_HOVER_SHOW = {
folderrow:
"group-hover/folderrow:opacity-100 group-has-[button:focus-visible]/folderrow:opacity-100",
labelrow:
"group-hover/labelrow:opacity-100 group-has-[button:focus-visible]/labelrow:opacity-100",
catnav:
"group-hover/catnav:opacity-100 group-has-[button:focus-visible]/catnav:opacity-100",
} as const
export function SidebarOverflowColumn({
unread,
menuOpen,
hoverGroup,
isSelected,
hasUnread,
className,
showMenuButton = true,
children,
}: {
unread: number
menuOpen: boolean
hoverGroup: "folderrow" | "labelrow" | "catnav"
isSelected?: boolean
hasUnread?: boolean
className?: string
showMenuButton?: boolean
children?: ReactNode
}) {
if (!showMenuButton) {
if (unread <= 0) return null
return (
<div className={cn("relative h-8 w-8 shrink-0", className)}>
<span
className={cn(
"flex h-full items-center justify-center text-xs tabular-nums leading-none",
isSelected && "font-medium",
hasUnread && !isSelected && "font-semibold"
)}
>
{formatCount(unread)}
</span>
</div>
)
}
return (
<div className={cn("relative h-8 w-8 shrink-0", className)}>
{unread > 0 && (
<span
className={cn(
"pointer-events-none absolute inset-0 flex items-center justify-center text-xs tabular-nums leading-none transition-opacity duration-150",
isSelected && "font-medium",
hasUnread && !isSelected && "font-semibold",
menuOpen ? "opacity-0" : OVERFLOW_COUNT_HOVER_HIDE[hoverGroup]
)}
>
{formatCount(unread)}
</span>
)}
<div
className={cn(
"absolute inset-0 flex items-center justify-center opacity-0 transition-opacity duration-150",
menuOpen ? "opacity-100" : OVERFLOW_MENU_HOVER_SHOW[hoverGroup]
)}
>
{children}
</div>
</div>
)
}
export const sidebarOverflowMenuButtonClass = MAIL_SIDEBAR_OVERFLOW_BTN_CLASS