295 lines
7.5 KiB
TypeScript
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
|