Refactor: Adapt limelight nav to sidebar

Adapt the provided limelight navigation component to the existing vertical sidebar, adjusting spacing and layout for improved aesthetics.
This commit is contained in:
gpt-engineer-app[bot] 2025-06-23 01:05:56 +00:00
parent 31cff64a24
commit 5b7df8445e
2 changed files with 222 additions and 48 deletions

View File

@ -1,8 +1,9 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';
import { useIsMobile } from '@/hooks/use-mobile';
import { ModernSidebar, SidebarBody, SidebarLink } from '@/components/ui/modern-sidebar';
import { ModernSidebar, SidebarBody } from '@/components/ui/modern-sidebar';
import { VerticalLimelightNav } from '@/components/ui/limelight-nav';
import OnboardingTour from '@/components/onboarding/OnboardingTour';
import HelpIcon from '@/components/help/HelpIcon';
import { useOnboardingTour } from '@/hooks/useOnboardingTour';
@ -63,6 +64,7 @@ const LogoIcon = () => {
export default function ModernLayout({ children }: ModernLayoutProps) {
const [open, setOpen] = useState(false);
const location = useLocation();
const isMobile = useIsMobile();
const {
@ -73,65 +75,82 @@ export default function ModernLayout({ children }: ModernLayoutProps) {
closeTour
} = useOnboardingTour();
const links = [
const mainLinks = [
{
id: "dashboard",
label: "Dashboard",
href: "/",
icon: <Home className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
icon: <Home />,
},
{
id: "transacoes",
label: "Transações",
href: "/transacoes",
icon: <Receipt className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
icon: <Receipt />,
},
{
id: "cartoes",
label: "Cartões de Crédito",
href: "/cartoes",
icon: <CreditCard className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
icon: <CreditCard />,
},
{
id: "categorias",
label: "Categorias",
href: "/categorias",
icon: <ListFilter className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
icon: <ListFilter />,
},
{
id: "metas",
label: "Metas",
href: "/metas",
icon: <Target className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
icon: <Target />,
},
{
id: "calendario",
label: "Calendário",
href: "/calendario",
icon: <Calendar className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
icon: <Calendar />,
},
{
id: "assinatura",
label: "Assinatura",
href: "/assinatura",
icon: <Crown className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
icon: <Crown />,
}
];
const whatsappLinks = [
{
id: "whatsapp",
label: "Conectar WhatsApp",
href: "/whatsapp",
icon: <MessageSquareText className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" data-tour="whatsapp-menu" />,
icon: <MessageSquareText data-tour="whatsapp-menu" />,
},
{
id: "grupos",
label: "Grupos",
href: "/grupos-whatsapp",
icon: <Users className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" data-tour="grupos-menu" />,
icon: <Users data-tour="grupos-menu" />,
}
];
const configLinks = [
{
id: "configuracoes",
label: "Configurações",
href: "/configuracoes",
icon: <Settings className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
icon: <Settings />,
}
];
// Determine active index based on current route
const getActiveIndex = () => {
const allLinks = [...mainLinks, ...whatsappLinks, ...configLinks];
const activeIndex = allLinks.findIndex(link => link.href === location.pathname);
return activeIndex >= 0 ? activeIndex : 0;
};
return (
<div className="min-h-screen bg-background flex w-full">
<ModernSidebar open={open} setOpen={setOpen}>
@ -139,43 +158,53 @@ export default function ModernLayout({ children }: ModernLayoutProps) {
<div className="flex flex-1 flex-col overflow-x-hidden overflow-y-auto h-full">
{open ? <Logo /> : <LogoIcon />}
<div className="mt-8 flex flex-col gap-2">
{links.map((link, idx) => (
<SidebarLink key={idx} link={link} />
))}
</div>
<div className="mt-8">
{open && (
<motion.h2
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mb-2 px-2 text-sm font-semibold tracking-tight text-gray-600"
>
WhatsApp
</motion.h2>
)}
<div className="flex flex-col gap-2">
{whatsappLinks.map((link, idx) => (
<SidebarLink key={`whatsapp-${idx}`} link={link} />
))}
<div className="mt-8 flex flex-col gap-6 flex-1 pb-4">
{/* Main Navigation */}
<div>
<VerticalLimelightNav
items={mainLinks}
defaultActiveIndex={getActiveIndex() < mainLinks.length ? getActiveIndex() : 0}
showLabels={open}
className="space-y-1"
/>
</div>
</div>
<div className="mt-auto">
{open && (
<motion.h2
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mb-2 px-2 text-sm font-semibold tracking-tight text-gray-600"
>
Configurações
</motion.h2>
)}
<div className="flex flex-col gap-2 pb-4">
{configLinks.map((link, idx) => (
<SidebarLink key={`config-${idx}`} link={link} />
))}
{/* WhatsApp Section */}
<div className="flex-1">
{open && (
<motion.h2
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mb-3 px-3 text-xs font-semibold tracking-tight text-gray-500 uppercase"
>
WhatsApp
</motion.h2>
)}
<VerticalLimelightNav
items={whatsappLinks}
defaultActiveIndex={getActiveIndex() >= mainLinks.length && getActiveIndex() < mainLinks.length + whatsappLinks.length ? getActiveIndex() - mainLinks.length : -1}
showLabels={open}
className="space-y-1"
/>
</div>
{/* Config Section */}
<div className="mt-auto">
{open && (
<motion.h2
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mb-3 px-3 text-xs font-semibold tracking-tight text-gray-500 uppercase"
>
Configurações
</motion.h2>
)}
<VerticalLimelightNav
items={configLinks}
defaultActiveIndex={getActiveIndex() >= mainLinks.length + whatsappLinks.length ? getActiveIndex() - mainLinks.length - whatsappLinks.length : -1}
showLabels={open}
className="space-y-1"
/>
</div>
</div>
</div>

View File

@ -0,0 +1,145 @@
import React, { useState, useRef, useLayoutEffect, cloneElement } from 'react';
// --- Internal Types and Defaults ---
const DefaultHomeIcon = (props: React.SVGProps<SVGSVGElement>) => <svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /></svg>;
const DefaultCompassIcon = (props: React.SVGProps<SVGSVGElement>) => <svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><path d="m16.24 7.76-2.12 6.36-6.36 2.12 2.12-6.36 6.36-2.12z" /></svg>;
const DefaultBellIcon = (props: React.SVGProps<SVGSVGElement>) => <svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" /><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" /></svg>;
export type NavItem = {
id: string | number;
icon: React.ReactElement;
label?: string;
onClick?: () => void;
href?: string;
};
const defaultNavItems: NavItem[] = [
{ id: 'default-home', icon: <DefaultHomeIcon />, label: 'Home' },
{ id: 'default-explore', icon: <DefaultCompassIcon />, label: 'Explore' },
{ id: 'default-notifications', icon: <DefaultBellIcon />, label: 'Notifications' },
];
type VerticalLimelightNavProps = {
items?: NavItem[];
defaultActiveIndex?: number;
onTabChange?: (index: number) => void;
className?: string;
limelightClassName?: string;
iconContainerClassName?: string;
iconClassName?: string;
showLabels?: boolean;
};
/**
* A vertical navigation bar with a "limelight" effect that highlights the active item.
*/
export const VerticalLimelightNav = ({
items = defaultNavItems,
defaultActiveIndex = 0,
onTabChange,
className,
limelightClassName,
iconContainerClassName,
iconClassName,
showLabels = true,
}: VerticalLimelightNavProps) => {
const [activeIndex, setActiveIndex] = useState(defaultActiveIndex);
const [isReady, setIsReady] = useState(false);
const navItemRefs = useRef<(HTMLAnchorElement | null)[]>([]);
const limelightRef = useRef<HTMLDivElement | null>(null);
useLayoutEffect(() => {
if (items.length === 0) return;
const limelight = limelightRef.current;
const activeItem = navItemRefs.current[activeIndex];
if (limelight && activeItem) {
const newTop = activeItem.offsetTop + activeItem.offsetHeight / 2 - limelight.offsetHeight / 2;
limelight.style.top = `${newTop}px`;
if (!isReady) {
setTimeout(() => setIsReady(true), 50);
}
}
}, [activeIndex, isReady, items]);
if (items.length === 0) {
return null;
}
const handleItemClick = (index: number, item: NavItem) => {
setActiveIndex(index);
onTabChange?.(index);
item.onClick?.();
};
return (
<nav className={`relative flex flex-col gap-1 py-2 ${className}`}>
{items.map((item, index) => {
const { id, icon, label, href } = item;
const isActive = activeIndex === index;
const content = (
<>
{cloneElement(icon, {
className: `w-5 h-5 shrink-0 transition-all duration-200 ${
isActive ? 'text-blue-700' : 'text-neutral-700 dark:text-neutral-200'
} ${icon.props.className || ''} ${iconClassName || ''}`,
})}
{showLabels && label && (
<span className={`text-sm font-medium transition-all duration-200 whitespace-nowrap ${
isActive ? 'text-blue-700' : 'text-neutral-700 dark:text-neutral-200'
}`}>
{label}
</span>
)}
</>
);
const baseClasses = `relative z-20 flex items-center gap-3 px-3 py-3 rounded-lg cursor-pointer transition-all duration-200 hover:bg-gradient-to-r hover:from-blue-50 hover:to-indigo-50 hover:text-blue-700 ${iconContainerClassName}`;
if (href) {
return (
<a
key={id}
ref={el => (navItemRefs.current[index] = el)}
href={href}
className={baseClasses}
onClick={() => handleItemClick(index, item)}
aria-label={label}
>
{content}
</a>
);
}
return (
<div
key={id}
ref={el => (navItemRefs.current[index] = el)}
className={baseClasses}
onClick={() => handleItemClick(index, item)}
aria-label={label}
>
{content}
</div>
);
})}
<div
ref={limelightRef}
className={`absolute left-0 z-10 w-1 h-8 rounded-full bg-blue-600 shadow-[5px_0_15px_rgba(29,78,216,0.3)] ${
isReady ? 'transition-[top] duration-400 ease-in-out' : ''
} ${limelightClassName}`}
style={{ top: '-999px' }}
>
<div className="absolute left-[4px] top-[-30%] w-12 h-[160%] [clip-path:polygon(0_5%,100%_25%,100%_75%,0_95%)] bg-gradient-to-r from-blue-600/20 to-transparent pointer-events-none" />
</div>
</nav>
);
};
export { VerticalLimelightNav as LimelightNav };