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 (
-
+
-
-
-
+
+
);
}
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 {