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 { 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>
|
||||
|
||||
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