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:
parent
31cff64a24
commit
5b7df8445e
@ -1,8 +1,9 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
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 { 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 OnboardingTour from '@/components/onboarding/OnboardingTour';
|
||||||
import HelpIcon from '@/components/help/HelpIcon';
|
import HelpIcon from '@/components/help/HelpIcon';
|
||||||
import { useOnboardingTour } from '@/hooks/useOnboardingTour';
|
import { useOnboardingTour } from '@/hooks/useOnboardingTour';
|
||||||
@ -63,6 +64,7 @@ const LogoIcon = () => {
|
|||||||
|
|
||||||
export default function ModernLayout({ children }: ModernLayoutProps) {
|
export default function ModernLayout({ children }: ModernLayoutProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const location = useLocation();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -73,65 +75,82 @@ export default function ModernLayout({ children }: ModernLayoutProps) {
|
|||||||
closeTour
|
closeTour
|
||||||
} = useOnboardingTour();
|
} = useOnboardingTour();
|
||||||
|
|
||||||
const links = [
|
const mainLinks = [
|
||||||
{
|
{
|
||||||
|
id: "dashboard",
|
||||||
label: "Dashboard",
|
label: "Dashboard",
|
||||||
href: "/",
|
href: "/",
|
||||||
icon: <Home className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
|
icon: <Home />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "transacoes",
|
||||||
label: "Transações",
|
label: "Transações",
|
||||||
href: "/transacoes",
|
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",
|
label: "Cartões de Crédito",
|
||||||
href: "/cartoes",
|
href: "/cartoes",
|
||||||
icon: <CreditCard className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
|
icon: <CreditCard />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "categorias",
|
||||||
label: "Categorias",
|
label: "Categorias",
|
||||||
href: "/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",
|
label: "Metas",
|
||||||
href: "/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",
|
label: "Calendário",
|
||||||
href: "/calendario",
|
href: "/calendario",
|
||||||
icon: <Calendar className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
|
icon: <Calendar />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "assinatura",
|
||||||
label: "Assinatura",
|
label: "Assinatura",
|
||||||
href: "/assinatura",
|
href: "/assinatura",
|
||||||
icon: <Crown className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />,
|
icon: <Crown />,
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const whatsappLinks = [
|
const whatsappLinks = [
|
||||||
{
|
{
|
||||||
|
id: "whatsapp",
|
||||||
label: "Conectar WhatsApp",
|
label: "Conectar WhatsApp",
|
||||||
href: "/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",
|
label: "Grupos",
|
||||||
href: "/grupos-whatsapp",
|
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 = [
|
const configLinks = [
|
||||||
{
|
{
|
||||||
|
id: "configuracoes",
|
||||||
label: "Configurações",
|
label: "Configurações",
|
||||||
href: "/configuracoes",
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-background flex w-full">
|
<div className="min-h-screen bg-background flex w-full">
|
||||||
<ModernSidebar open={open} setOpen={setOpen}>
|
<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">
|
<div className="flex flex-1 flex-col overflow-x-hidden overflow-y-auto h-full">
|
||||||
{open ? <Logo /> : <LogoIcon />}
|
{open ? <Logo /> : <LogoIcon />}
|
||||||
|
|
||||||
<div className="mt-8 flex flex-col gap-2">
|
<div className="mt-8 flex flex-col gap-6 flex-1 pb-4">
|
||||||
{links.map((link, idx) => (
|
{/* Main Navigation */}
|
||||||
<SidebarLink key={idx} link={link} />
|
<div>
|
||||||
))}
|
<VerticalLimelightNav
|
||||||
</div>
|
items={mainLinks}
|
||||||
|
defaultActiveIndex={getActiveIndex() < mainLinks.length ? getActiveIndex() : 0}
|
||||||
<div className="mt-8">
|
showLabels={open}
|
||||||
{open && (
|
className="space-y-1"
|
||||||
<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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-auto">
|
{/* WhatsApp Section */}
|
||||||
{open && (
|
<div className="flex-1">
|
||||||
<motion.h2
|
{open && (
|
||||||
initial={{ opacity: 0 }}
|
<motion.h2
|
||||||
animate={{ opacity: 1 }}
|
initial={{ opacity: 0 }}
|
||||||
className="mb-2 px-2 text-sm font-semibold tracking-tight text-gray-600"
|
animate={{ opacity: 1 }}
|
||||||
>
|
className="mb-3 px-3 text-xs font-semibold tracking-tight text-gray-500 uppercase"
|
||||||
Configurações
|
>
|
||||||
</motion.h2>
|
WhatsApp
|
||||||
)}
|
</motion.h2>
|
||||||
<div className="flex flex-col gap-2 pb-4">
|
)}
|
||||||
{configLinks.map((link, idx) => (
|
<VerticalLimelightNav
|
||||||
<SidebarLink key={`config-${idx}`} link={link} />
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
145
src/components/ui/limelight-nav.tsx
Normal file
145
src/components/ui/limelight-nav.tsx
Normal 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 };
|
||||||
Loading…
Reference in New Issue
Block a user