301 lines
11 KiB
TypeScript
301 lines
11 KiB
TypeScript
import { ArrowRight, PlayCircle } from "lucide-react";
|
|
import type { ReactNode } from "react";
|
|
import {
|
|
HoverCard,
|
|
HoverCardContent,
|
|
HoverCardTrigger,
|
|
} from "@/components/ui/hover-card";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import type { LovartHomeSidebarItemKey } from "@/lib/home/lovart-sidebar-config";
|
|
import type { NavLink, TutorialVideo } from "./types";
|
|
|
|
type SidebarItem = {
|
|
iconUrl?: string | null;
|
|
label: string;
|
|
};
|
|
|
|
type LovartSidebarProps = {
|
|
agentEnabled: boolean;
|
|
getItem: (key: LovartHomeSidebarItemKey) => SidebarItem;
|
|
isEnabled: (key: LovartHomeSidebarItemKey) => boolean;
|
|
isProjectsOnlyView: boolean;
|
|
onAllProjects: () => void;
|
|
onGallery: () => void;
|
|
onHome: () => void;
|
|
onImageEditor: () => void;
|
|
onNavLink: (url: string) => void;
|
|
onNewCanvas: () => void;
|
|
onRoute: (url: string) => void;
|
|
onTutorial: (video: TutorialVideo) => void;
|
|
productPlanningEnabled: boolean;
|
|
renderIcon: (
|
|
key: LovartHomeSidebarItemKey,
|
|
options?: {
|
|
active?: boolean;
|
|
filled?: boolean;
|
|
sizeClassName?: string;
|
|
}
|
|
) => ReactNode;
|
|
settingsHref: string | null;
|
|
siteLogo?: string | null;
|
|
siteName: string;
|
|
toolbox1Enabled: boolean;
|
|
toolbox1Links: NavLink[];
|
|
toolbox2Enabled: boolean;
|
|
toolbox2Links: NavLink[];
|
|
tutorialEnabled: boolean;
|
|
tutorialVideos: TutorialVideo[];
|
|
videoCanvasEnabled: boolean;
|
|
tone?: "light" | "dark";
|
|
};
|
|
|
|
export function LovartSidebar({
|
|
agentEnabled,
|
|
getItem,
|
|
isEnabled,
|
|
isProjectsOnlyView,
|
|
onAllProjects,
|
|
onGallery,
|
|
onHome,
|
|
onImageEditor,
|
|
onNavLink,
|
|
onNewCanvas,
|
|
onRoute,
|
|
onTutorial,
|
|
productPlanningEnabled,
|
|
renderIcon,
|
|
settingsHref,
|
|
siteLogo,
|
|
siteName,
|
|
toolbox1Enabled,
|
|
toolbox1Links,
|
|
toolbox2Enabled,
|
|
toolbox2Links,
|
|
tutorialEnabled,
|
|
tutorialVideos,
|
|
videoCanvasEnabled,
|
|
tone = "light",
|
|
}: LovartSidebarProps) {
|
|
const isDark = tone === "dark";
|
|
const sidebarPopupBaseClass =
|
|
"rounded-[24px] border border-black/8 bg-white/96 p-2.5 text-[#111827] shadow-[0_24px_64px_rgba(15,23,42,0.12)] backdrop-blur-xl dark:border-white/10 dark:bg-[#141416]/96 dark:text-white dark:shadow-[0_24px_64px_rgba(0,0,0,0.42)]";
|
|
const sidebarButtonClass = (active = false) =>
|
|
[
|
|
"group flex h-9 w-9 shrink-0 items-center justify-center rounded-[11px] text-[#202020] transition-colors hover:bg-[#f5f5f5] dark:text-white dark:hover:bg-white/10",
|
|
active
|
|
? "bg-[#f5f5f5] text-[#202020] dark:bg-white/10 dark:text-white"
|
|
: "",
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ");
|
|
|
|
const tooltipButton = (
|
|
key: LovartHomeSidebarItemKey,
|
|
onClick: () => void,
|
|
options?: { active?: boolean }
|
|
) =>
|
|
isEnabled(key) ? (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className={sidebarButtonClass(options?.active)}
|
|
onClick={onClick}
|
|
title={getItem(key).label}
|
|
>
|
|
{renderIcon(key, {
|
|
active: options?.active,
|
|
sizeClassName: "h-[18px] w-[18px]",
|
|
})}
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">{getItem(key).label}</TooltipContent>
|
|
</Tooltip>
|
|
) : null;
|
|
|
|
const toolbox = (
|
|
key: "toolbox1" | "toolbox2",
|
|
links: NavLink[],
|
|
enabled: boolean
|
|
) =>
|
|
isEnabled(key) && enabled && links.length > 0 ? (
|
|
<HoverCard openDelay={80} closeDelay={80}>
|
|
<HoverCardTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className={sidebarButtonClass()}
|
|
title={getItem(key).label}
|
|
>
|
|
{renderIcon(key, { sizeClassName: "h-[18px] w-[18px]" })}
|
|
</button>
|
|
</HoverCardTrigger>
|
|
<HoverCardContent
|
|
side="right"
|
|
align="start"
|
|
className={`w-[248px] ${sidebarPopupBaseClass}`}
|
|
>
|
|
<div className="px-2 pb-1 pt-1 text-xs font-medium text-black/40 dark:text-white/42">
|
|
{getItem(key).label}
|
|
</div>
|
|
<div className="space-y-1">
|
|
{links.map((link) => (
|
|
<button
|
|
key={link.id}
|
|
type="button"
|
|
className="flex w-full items-center justify-between rounded-2xl px-3 py-2.5 text-left text-[13px] text-black/80 transition hover:bg-black/[0.04] dark:text-white/80 dark:hover:bg-white/8"
|
|
onClick={() => onNavLink(link.url)}
|
|
>
|
|
<span className="line-clamp-1">{link.name}</span>
|
|
<ArrowRight className="h-3.5 w-3.5 text-black/30 dark:text-white/35" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</HoverCardContent>
|
|
</HoverCard>
|
|
) : null;
|
|
|
|
return (
|
|
<aside
|
|
className={[
|
|
"fixed bottom-0 left-0 top-0 z-[100] hidden w-[62px] flex-col items-center border-r lg:flex",
|
|
isDark
|
|
? "border-white/10 bg-black"
|
|
: "border-[#e9e9e9] bg-white",
|
|
].join(" ")}
|
|
>
|
|
<TooltipProvider delayDuration={120}>
|
|
<div className="flex h-full w-full flex-col items-center overflow-y-auto overflow-x-hidden px-2 pb-4 pt-[15px] [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-opacity hover:opacity-80"
|
|
onClick={onHome}
|
|
title={siteName}
|
|
>
|
|
{siteLogo ? (
|
|
<img
|
|
src={siteLogo}
|
|
alt={siteName}
|
|
className="h-8 w-8 rounded-full object-cover"
|
|
/>
|
|
) : (
|
|
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-black text-[13px] font-semibold text-white dark:bg-white dark:text-black">
|
|
{siteName.slice(0, 1).toUpperCase()}
|
|
</span>
|
|
)}
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">{siteName}</TooltipContent>
|
|
</Tooltip>
|
|
|
|
<div className="mt-6 flex w-full flex-col items-center gap-[18px]">
|
|
{isEnabled("newProject") ? (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
type="button"
|
|
onClick={onNewCanvas}
|
|
className={sidebarButtonClass()}
|
|
title={getItem("newProject").label}
|
|
>
|
|
{renderIcon("newProject", {
|
|
sizeClassName: "h-[18px] w-[18px]",
|
|
})}
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">
|
|
{getItem("newProject").label}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
) : null}
|
|
{tooltipButton("home", onHome, { active: !isProjectsOnlyView })}
|
|
{tooltipButton("projects", onAllProjects, { active: isProjectsOnlyView })}
|
|
{tooltipButton("gallery", onGallery)}
|
|
{isEnabled("tutorial") && tutorialEnabled ? (
|
|
<HoverCard openDelay={80} closeDelay={100}>
|
|
<HoverCardTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className={sidebarButtonClass()}
|
|
title={getItem("tutorial").label}
|
|
>
|
|
{renderIcon("tutorial", { sizeClassName: "h-[18px] w-[18px]" })}
|
|
</button>
|
|
</HoverCardTrigger>
|
|
<HoverCardContent
|
|
side="right"
|
|
align="start"
|
|
className={`w-[300px] ${sidebarPopupBaseClass}`}
|
|
>
|
|
<div className="px-2 pb-1 pt-1 text-xs font-medium text-black/40 dark:text-white/42">
|
|
{getItem("tutorial").label}
|
|
</div>
|
|
<div className="max-h-[380px] space-y-1 overflow-y-auto">
|
|
{tutorialVideos.length > 0 ? (
|
|
tutorialVideos.map((video) => (
|
|
<button
|
|
key={video.id}
|
|
type="button"
|
|
className="flex w-full items-center gap-2.5 rounded-2xl p-2.5 text-left transition hover:bg-black/[0.04] dark:hover:bg-white/8"
|
|
onClick={() => onTutorial(video)}
|
|
>
|
|
<div className="h-10 w-16 flex-shrink-0 overflow-hidden rounded-xl bg-black/[0.04] dark:bg-white/8">
|
|
{video.coverUrl ? (
|
|
<img
|
|
src={video.coverUrl}
|
|
alt={video.title}
|
|
className="h-full w-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="flex h-full w-full items-center justify-center">
|
|
<PlayCircle className="h-4 w-4 text-black/35 dark:text-white/45" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="truncate text-[13px] font-medium text-black/85 dark:text-white/88">
|
|
{video.title}
|
|
</div>
|
|
{video.description ? (
|
|
<div className="truncate text-[11px] text-black/42 dark:text-white/42">
|
|
{video.description}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</button>
|
|
))
|
|
) : (
|
|
<div className="px-2 py-3 text-[12px] text-black/42 dark:text-white/42">暂无教程视频</div>
|
|
)}
|
|
</div>
|
|
</HoverCardContent>
|
|
</HoverCard>
|
|
) : null}
|
|
{tooltipButton("imageEditor", onImageEditor)}
|
|
{tooltipButton("aiCad", () => onRoute("/ai-cad"))}
|
|
{tooltipButton("model3d", () => onRoute("/model3d"))}
|
|
{agentEnabled ? tooltipButton("agent", () => onRoute("/canvas?mode=agent")) : null}
|
|
{videoCanvasEnabled ? tooltipButton("videoCanvas", () => onRoute("/video-canvas")) : null}
|
|
{tooltipButton("flowBoard", () => onRoute("/flow-board"))}
|
|
{productPlanningEnabled
|
|
? tooltipButton("productPlanning", () => onRoute("/product-planning"))
|
|
: null}
|
|
{toolbox("toolbox1", toolbox1Links, toolbox1Enabled)}
|
|
{toolbox("toolbox2", toolbox2Links, toolbox2Enabled)}
|
|
</div>
|
|
|
|
<div className="mt-auto flex w-full flex-col items-center gap-1 pt-4">
|
|
{settingsHref ? tooltipButton("settings", () => onRoute(settingsHref)) : null}
|
|
</div>
|
|
</div>
|
|
</TooltipProvider>
|
|
</aside>
|
|
);
|
|
}
|