ultisuite-client/components/suite/suite-favorites-menu.tsx
R3D347HR4Y ad1370ea7e
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat: enhance configuration and add new demo layouts
- Introduced turbopack alias for canvas in next.config.mjs.
- Updated package.json scripts for development and branding tasks.
- Added new dependencies for Tiptap extensions.
- Implemented new demo layouts for agenda, contacts, drive, and mail applications.
- Enhanced globals.css for improved theming and splash screen animations.
- Added OAuth callback handling for drive mounts.
- Updated layout components to integrate new demo shells and improve structure.
2026-06-12 19:10:24 +02:00

187 lines
5.0 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 { useEffect, useRef, useState } from "react"
import Link from "next/link"
import { LayoutGrid, Pencil } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
SUITE_FAVORITE_APPS,
type FavoriteApp,
} from "@/lib/suite/favorite-apps"
import { SUITE_HEADER_DROPDOWN_CLASS, SUITE_ICON_BTN } from "@/lib/suite/suite-chrome-classes"
import { cn } from "@/lib/utils"
const FAVORITE_TILE_CLASS =
"flex flex-col items-center gap-2 rounded-lg p-3 transition-colors hover:bg-accent"
const DEFAULT_ICON_BTN_CLASS = cn(
"rounded-full",
SUITE_ICON_BTN,
"hover:text-accent-foreground",
)
function FavoriteAppTile({
app,
onNavigate,
}: {
app: FavoriteApp
onNavigate?: () => void
}) {
const content = (
<>
<div className="flex h-10 w-10 items-center justify-center">
{app.iconDark ? (
<>
<img
src={app.icon}
alt=""
className="h-10 w-10 object-contain dark:hidden"
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
}}
/>
<img
src={app.iconDark}
alt=""
className="hidden h-10 w-10 object-contain dark:block"
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
}}
/>
</>
) : (
<img
src={app.icon}
alt=""
className={cn(
"h-10 w-10 object-contain",
app.whiteLogoInDark && "dark:invert dark:hue-rotate-180",
)}
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
target.parentElement!.innerHTML = `<div class="flex h-10 w-10 items-center justify-center rounded-full bg-blue-500 font-bold text-white">${app.name[0]}</div>`
}}
/>
)}
</div>
<span className="w-full text-center text-xs text-muted-foreground">
{app.name}
</span>
</>
)
if (!app.href) {
return (
<button type="button" className={FAVORITE_TILE_CLASS} disabled>
{content}
</button>
)
}
if (app.external) {
return (
<a
href={app.href}
target="_blank"
rel="noopener noreferrer"
className={FAVORITE_TILE_CLASS}
onClick={onNavigate}
>
{content}
</a>
)
}
return (
<Link href={app.href} className={FAVORITE_TILE_CLASS} onClick={onNavigate}>
{content}
</Link>
)
}
interface SuiteFavoritesMenuProps {
className?: string
iconButtonClass?: string
dropdownClass?: string
/** Fermer un autre panneau header (ex. compte) à louverture. */
onOpen?: () => void
}
export function SuiteFavoritesMenu({
className,
iconButtonClass = DEFAULT_ICON_BTN_CLASS,
dropdownClass = SUITE_HEADER_DROPDOWN_CLASS,
onOpen,
}: SuiteFavoritesMenuProps) {
const [open, setOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpen(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => document.removeEventListener("mousedown", handleClickOutside)
}, [])
return (
<div className={cn("relative hidden sm:block", className)} ref={menuRef}>
<Button
variant="ghost"
size="icon"
className={iconButtonClass}
aria-label="Applications"
aria-expanded={open}
aria-haspopup="dialog"
onClick={() => {
const next = !open
setOpen(next)
if (next) onOpen?.()
}}
>
<LayoutGrid className="size-6 shrink-0" aria-hidden />
</Button>
{open ? (
<div
className={cn(
"absolute right-0 top-12 z-50 w-96 rounded-2xl",
dropdownClass,
)}
role="dialog"
aria-label="Vos favoris"
>
<div className="flex items-center justify-between border-b border-border p-4">
<span className="text-lg font-normal text-foreground">
Vos favoris
</span>
<Button
variant="ghost"
size="icon"
className={cn("h-8 w-8", iconButtonClass)}
aria-label="Personnaliser les favoris"
disabled
>
<Pencil className="h-4 w-4" />
</Button>
</div>
<div className="grid grid-cols-3 gap-1 p-3">
{SUITE_FAVORITE_APPS.map((app) => (
<FavoriteAppTile
key={app.name}
app={app}
onNavigate={() => setOpen(false)}
/>
))}
</div>
</div>
) : null}
</div>
)
}