diff --git a/apps/apollo-vertex/registry.json b/apps/apollo-vertex/registry.json index 170d79712..50278a7e3 100644 --- a/apps/apollo-vertex/registry.json +++ b/apps/apollo-vertex/registry.json @@ -506,6 +506,10 @@ { "path": "registry/collapsible/collapsible.tsx", "type": "registry:ui" + }, + { + "path": "registry/collapsible/collapsible.css", + "type": "registry:ui" } ] }, @@ -953,7 +957,6 @@ "description": "Provides UiPath authentication and renders children only when authenticated.", "dependencies": [ "lucide-react", - "framer-motion", "react-i18next", "i18next", "jwt-decode", @@ -962,14 +965,16 @@ "@tanstack/react-router", "pkce-challenge", "sonner", - "@mantine/hooks@^9.0.0" + "framer-motion" ], "registryDependencies": [ "button", "tooltip", "avatar", "dropdown-menu", - "spinner" + "skeleton", + "sidebar", + "collapsible" ], "files": [ { "path": "registry/shell/shell.tsx", "type": "registry:ui" }, @@ -1004,14 +1009,6 @@ { "path": "lib/i18n.ts", "type": "registry:lib" }, { "path": "lib/auth.ts", "type": "registry:lib" }, { "path": "lib/react-i18next.d.ts", "type": "registry:lib" }, - { - "path": "registry/shell/shell-animations.ts", - "type": "registry:ui" - }, - { - "path": "registry/shell/shell-nav-item.tsx", - "type": "registry:ui" - }, { "path": "registry/shell/shell-minimal-company.tsx", "type": "registry:ui" @@ -1024,10 +1021,6 @@ "path": "registry/shell/shell-text.tsx", "type": "registry:ui" }, - { - "path": "registry/shell/shell-theme-toggle.tsx", - "type": "registry:ui" - }, { "path": "registry/shell/shell-theme-provider.tsx", "type": "registry:ui" @@ -1055,6 +1048,10 @@ { "path": "registry/shell/shell-user-profile-menu-items.tsx", "type": "registry:ui" + }, + { + "path": "registry/shell/shell-animations.ts", + "type": "registry:ui" } ] }, @@ -1139,6 +1136,7 @@ "dependencies": [ "@radix-ui/react-slot", "class-variance-authority", + "framer-motion", "lucide-react", "react-i18next" ], diff --git a/apps/apollo-vertex/registry/shell/shell-layout.tsx b/apps/apollo-vertex/registry/shell/shell-layout.tsx index d0056767b..71d728139 100644 --- a/apps/apollo-vertex/registry/shell/shell-layout.tsx +++ b/apps/apollo-vertex/registry/shell/shell-layout.tsx @@ -1,6 +1,12 @@ import type { PropsWithChildren } from "react"; +import { useId } from "react"; +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar"; import type { CompanyLogo, ShellNavItem } from "./shell"; -import { Sidebar } from "./shell-sidebar"; +import { ShellSidebar } from "./shell-sidebar"; import { useTheme } from "./shell-theme-provider"; const GRADIENT_BLUR = "blur(149.643px)"; @@ -14,6 +20,8 @@ interface ShellLayoutProps { } function DarkGradientBackground() { + const filterId = useId(); + return (
{/* Base directional wash */} @@ -74,7 +82,7 @@ function DarkGradientBackground() { className="absolute inset-0 w-full h-full opacity-[0.04]" > - + - +
); @@ -150,7 +158,7 @@ function LightGradientBackground() { function GradientBackground() { const theme = useTheme(); - if (theme.theme === "dark") { + if (theme.resolvedTheme === "dark") { return ; } return ; @@ -169,36 +177,46 @@ export function ShellLayout({
- -
- {children} -
+
{children}
); } return ( -
+ - -
-
+ +
+ +
+
{children}
-
-
+ + ); } diff --git a/apps/apollo-vertex/registry/shell/shell-nav-item.tsx b/apps/apollo-vertex/registry/shell/shell-nav-item.tsx deleted file mode 100644 index ad22ad73b..000000000 --- a/apps/apollo-vertex/registry/shell/shell-nav-item.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Link, useLocation } from "@tanstack/react-router"; -import { useLocalStorage } from "@mantine/hooks"; -import { AnimatePresence, motion } from "framer-motion"; -import type { LucideIcon } from "lucide-react"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; -import { - fastFadeTransition, - iconHoverScale, - textFadeVariants, -} from "./shell-animations"; -import { SIDEBAR_COLLAPSED_KEY } from "./shell-constants"; -import { Text } from "./shell-text"; -import type { TranslationKey } from "./shell-translation-key"; - -interface NavItemProps { - to: string; - icon: LucideIcon; - label: TranslationKey; -} - -export const NavItem = ({ to, icon: Icon, label }: NavItemProps) => { - const [isCollapsed] = useLocalStorage({ - key: SIDEBAR_COLLAPSED_KEY, - defaultValue: false, - }); - const { pathname } = useLocation(); - const isActive = pathname === to || pathname.startsWith(`${to}/`); - - const linkContent = ( - - - - - - {!isCollapsed && ( - - - - )} - - - ); - - if (isCollapsed) { - return ( - - - {linkContent} - - - - - - ); - } - - return linkContent; -}; diff --git a/apps/apollo-vertex/registry/shell/shell-sidebar.tsx b/apps/apollo-vertex/registry/shell/shell-sidebar.tsx index affd72f03..944409691 100644 --- a/apps/apollo-vertex/registry/shell/shell-sidebar.tsx +++ b/apps/apollo-vertex/registry/shell/shell-sidebar.tsx @@ -1,16 +1,43 @@ -import { useLocalStorage } from "@mantine/hooks"; -import { motion } from "framer-motion"; +import { Link, useLocation } from "@tanstack/react-router"; +import { AnimatePresence, motion } from "framer-motion"; +import { ChevronDown } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + useSidebar, +} from "@/components/ui/sidebar"; import { cn } from "@/lib/utils"; import type { CompanyLogo, ShellNavItem } from "./shell"; -import { sidebarSpring } from "./shell-animations"; +import { fastFadeTransition, textFadeVariants } from "./shell-animations"; import { Company } from "./shell-company"; -import { SIDEBAR_COLLAPSED_KEY } from "./shell-constants"; import { MinimalCompany } from "./shell-minimal-company"; import { MinimalNavItem } from "./shell-minimal-nav-item"; -import { NavItem } from "./shell-nav-item"; +import { Text } from "./shell-text"; import { UserProfile } from "./shell-user-profile"; -interface SidebarProps { +const activeNavClass = + "text-sidebar-foreground/85 hover:text-sidebar-foreground data-[active=true]:text-primary-700 dark:data-[active=true]:text-primary-400 data-[active=true]:font-semibold data-[active=true]:bg-primary-100/40 dark:data-[active=true]:bg-primary-900/30"; +const navButtonClass = `font-medium ${activeNavClass}`; +const subButtonClass = activeNavClass; + +interface ShellSidebarProps { companyName: string; productName: string; variant?: "minimal"; @@ -18,20 +45,13 @@ interface SidebarProps { navItems: ShellNavItem[]; } -export const Sidebar = ({ +export const ShellSidebar = ({ companyName, productName, variant, companyLogo, navItems, -}: SidebarProps) => { - const [isCollapsed] = useLocalStorage({ - key: SIDEBAR_COLLAPSED_KEY, - defaultValue: false, - }); - - const sidebarWidth = isCollapsed ? "w-16" : "w-[280px]"; - +}: ShellSidebarProps) => { if (variant === "minimal") { return (
@@ -41,59 +61,258 @@ export const Sidebar = ({ companyLogo={companyLogo} /> - {navItems.length > 0 && ( - - )} +
- +
); } return ( - + ); +}; + +interface SidebarNavProps { + companyName: string; + productName: string; + companyLogo?: CompanyLogo; + navItems: ShellNavItem[]; +} + +function SidebarNav({ + companyName, + productName, + companyLogo, + navItems, +}: SidebarNavProps) { + const { t } = useTranslation(); + const { state, toggleSidebar } = useSidebar(); + const { pathname } = useLocation(); + const isCollapsed = state === "collapsed"; + const [sidebarHovered, setSidebarHovered] = useState(false); + + const handleMouseEnter = () => { + if (isCollapsed) setSidebarHovered(true); + }; + + const handleMouseLeave = () => { + setSidebarHovered(false); + }; + + const [expandedItems, setExpandedItems] = useState>(() => { + const expanded = new Set(); + for (const item of navItems) { + if (item.subItems?.some((sub) => pathname.startsWith(sub.path))) { + expanded.add(item.path); + } + } + return expanded; + }); + + useEffect(() => { + const expanded = new Set(); + for (const item of navItems) { + if (item.subItems?.some((sub) => pathname.startsWith(sub.path))) { + expanded.add(item.path); + } + } + setExpandedItems(expanded); + }, [pathname, navItems]); + + const handleMenuClick = (path: string) => { + if (isCollapsed) { + toggleSidebar(); + setExpandedItems((prev) => new Set(prev).add(path)); + } else { + setExpandedItems((prev) => { + const next = new Set(prev); + if (next.has(path)) { + next.delete(path); + } else { + next.add(path); + } + return next; + }); + } + }; + + const isActive = (path: string) => { + if (path === "/") { + return pathname === "/"; + } + return pathname === path || pathname.startsWith(`${path}/`); + }; + + const isParentActive = (item: ShellNavItem) => { + return item.subItems?.some((sub) => isActive(sub.path)) ?? false; + }; + + const getTooltipText = (label: ShellNavItem["label"]): string => { + if (typeof label === "string") return t(label); + return t(label.i18nKey, label.values); + }; + + return ( + - - -
+ + + + + + + + + {navItems.map((item) => { + const Icon = item.icon; + const active = isActive(item.path); + const parentActive = isParentActive(item); + const isExpanded = expandedItems.has(item.path); + + if (item.subItems && item.subItems.length > 0) { + const showParentActive = isCollapsed && parentActive; + + return ( + handleMenuClick(item.path)} + className="group/collapsible" + > + + + + + + {!isCollapsed && ( + + + + )} + + + {!isCollapsed && ( + + + + )} + + + + + + {item.subItems.map((subItem) => { + const subActive = isActive(subItem.path); + return ( + + + + + + + + ); + })} + + + + + ); + } + + return ( + + + + + + {!isCollapsed && ( + + + + )} + + + + + ); + })} + + + + + + -
+ +
- + ); -}; +} diff --git a/apps/apollo-vertex/registry/shell/shell-user-profile.tsx b/apps/apollo-vertex/registry/shell/shell-user-profile.tsx index 805e00e32..043ef2716 100644 --- a/apps/apollo-vertex/registry/shell/shell-user-profile.tsx +++ b/apps/apollo-vertex/registry/shell/shell-user-profile.tsx @@ -4,110 +4,180 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { DropdownMenu, DropdownMenuContent, + DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { sidebarSpring } from "./shell-animations"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { + fastFadeTransition, + iconHoverScale, + textFadeVariants, +} from "./shell-animations"; import { UserProfileMenuItems } from "./shell-user-profile-menu-items"; import { useUser } from "./shell-user-provider"; interface UserProfileProps { - isCollapsed: boolean; + isCollapsed?: boolean; + isMinimal?: boolean; } -export const UserProfile = ({ isCollapsed }: UserProfileProps) => { +export const UserProfile = ({ isCollapsed, isMinimal }: UserProfileProps) => { const { t } = useTranslation(); const { user } = useUser(); const userInitials = user ? user.name .split(" ") - .map((n: string) => n[0]) + .map((n) => n[0]) .join("") .toUpperCase() .slice(0, 2) : "U"; - const firstName = user?.first_name ?? t("business_user"); - const lastName = user?.last_name ?? ""; + const userName = user?.name ?? t("business_user"); + const userEmail = user?.email ?? t("user_email_placeholder"); - return ( - - {isCollapsed ? ( - - - - - - {userInitials} - - - - - -
-
- - {user?.name ?? t("business_user")} - - - {user?.email ?? t("user_email_placeholder")} - -
+ const avatarElement = ( + + + + {userInitials} + + + + ); + + if (isMinimal) { + return ( + + + + + + + + + + {userName} + + + + +
+
+ {userName} + {userEmail}
- - - - - ) : ( - - - - - - {userInitials} - - -
- - {firstName} {lastName} - - - {user?.email ?? t("user_email_placeholder")} - -
-
-
- - - -
+
+ + +
+
+ ); + } + + const trigger = ( + + ); + + const dropdownContent = isCollapsed ? ( + + +
+ {userName} + + {userEmail} + +
+
+ + +
+ ) : ( + + + + ); + + return ( + + + + + {trigger} + + + {t("user_profile")} + + + + {dropdownContent} + ); }; diff --git a/apps/apollo-vertex/registry/shell/shell.tsx b/apps/apollo-vertex/registry/shell/shell.tsx index b7b9a6bb1..b01d6392d 100644 --- a/apps/apollo-vertex/registry/shell/shell.tsx +++ b/apps/apollo-vertex/registry/shell/shell.tsx @@ -12,12 +12,19 @@ export interface CompanyLogo { url: string; darkUrl?: string; alt: string; + isCustom?: boolean; +} + +export interface ShellSubNavItem { + path: string; + label: TranslationKey; } export interface ShellNavItem { path: string; label: TranslationKey; icon: LucideIcon; + subItems?: ShellSubNavItem[]; } export interface ApolloShellProps extends PropsWithChildren {