-
-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add-new-bottom-navigation-menu-&-home-page (#50)
* ✨ add new floating dock menu * 🎨 improve scrollarea to accept viewport * ✨ display sidemenu corresponding to category selected * ✅ remove outdated HomePage tests and update Sidemenu tests to include InfoMenuList * 🎨 add components to preview only variants * ✨ update dev script to use turbo mode for improved performance
- Loading branch information
1 parent
205f56e
commit 3bc4536
Showing
30 changed files
with
820 additions
and
561 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,29 @@ | ||
import { DesktopSideMenu } from "#/src/components/navigation/desktop-menu"; | ||
|
||
import { AddressBar } from "#/src/ui/address-bar"; | ||
import StarGithubProjectButton from "#/src/ui/star-github-project-button"; | ||
import type React from "react"; | ||
|
||
export default function ComponentsLayout({ | ||
children, | ||
children, | ||
}: Readonly<{ children: React.ReactNode }>) { | ||
return <div className="space-y-10">{children}</div>; | ||
return ( | ||
<div className="mx-auto max-w-screen-2xl"> | ||
<DesktopSideMenu /> | ||
|
||
<div className="lg:ml-80"> | ||
{/* Add overflow-auto if layout width problems */} | ||
|
||
<AddressBar /> | ||
|
||
<main className=" p-4 pt-12 pb-20 md:p-6"> | ||
<div className="space-y-10">{children}</div> | ||
</main> | ||
|
||
<div className="flex sm:hidden fixed bottom-6 left-2"> | ||
<StarGithubProjectButton /> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,261 @@ | ||
"use client"; | ||
import { useOnClickOutside } from "@/cuicui/hooks/use-click-outside/use-click-outside"; | ||
/** | ||
* Note: Use position fixed according to your needs | ||
* Desktop navbar is better positioned at the bottom | ||
* Mobile navbar is better positioned at bottom right. | ||
**/ | ||
|
||
import { cn } from "@/cuicui/utils/cn/cn"; | ||
import { | ||
AnimatePresence, | ||
type MotionValue, | ||
motion, | ||
useMotionValue, | ||
useSpring, | ||
useTransform, | ||
} from "framer-motion"; | ||
import { ListCollapseIcon, type LucideIcon } from "lucide-react"; | ||
import Link from "next/link"; | ||
import { usePathname } from "next/navigation"; | ||
import { useEffect, useRef, useState } from "react"; | ||
|
||
export const FloatingDock = ({ | ||
items, | ||
desktopClassName, | ||
mobileClassName, | ||
}: { | ||
items: { | ||
title: string; | ||
Icon: LucideIcon; | ||
href: string; | ||
}[]; | ||
desktopClassName?: string; | ||
mobileClassName?: string; | ||
}) => { | ||
return ( | ||
<> | ||
<FloatingDockDesktop items={items} className={desktopClassName} /> | ||
<FloatingDockMobile items={items} className={mobileClassName} /> | ||
</> | ||
); | ||
}; | ||
|
||
const FloatingDockMobile = ({ | ||
items, | ||
className, | ||
}: { | ||
items: { | ||
title: string; | ||
Icon: LucideIcon; | ||
href: string; | ||
}[]; | ||
className?: string; | ||
}) => { | ||
const ref = useRef<HTMLDivElement>(null); | ||
const handleClickOutside = (event: MouseEvent | TouchEvent | FocusEvent) => { | ||
setOpen(false); | ||
}; | ||
|
||
const pathname = usePathname(); | ||
|
||
useEffect(() => { | ||
if (!pathname) { | ||
return; | ||
} | ||
setOpen(false); | ||
}, [pathname]); | ||
|
||
useOnClickOutside(ref, handleClickOutside); | ||
const [open, setOpen] = useState(false); | ||
return ( | ||
<div ref={ref} className={cn("relative block md:hidden ", className)}> | ||
<AnimatePresence> | ||
{open && ( | ||
<motion.div | ||
layoutId="nav" | ||
className="absolute bottom-full mb-2 inset-x-0 flex flex-col items-end gap-2" | ||
> | ||
{items.map((item, idx) => ( | ||
<motion.div | ||
key={item.title} | ||
initial={{ opacity: 0, y: 10 }} | ||
animate={{ | ||
opacity: 1, | ||
y: 0, | ||
}} | ||
exit={{ | ||
opacity: 0, | ||
y: 10, | ||
transition: { | ||
delay: idx * 0.05, | ||
}, | ||
}} | ||
transition={{ delay: (items.length - 1 - idx) * 0.05 }} | ||
> | ||
<Link | ||
href={item.href} | ||
key={item.title} | ||
className={cn( | ||
"rounded-full w-fit px-4 py-4 bg-neutral-50 border border-neutral-400/20 flex items-center justify-center gap-2", | ||
pathname.includes(item.href) | ||
? "bg-neutral-700 dark:bg-neutral-200 text-neutral-100 dark:text-neutral-800" | ||
: "dark:bg-neutral-900 text-neutral-500 dark:text-neutral-300", | ||
)} | ||
onClick={() => setOpen(false)} | ||
> | ||
<item.Icon className="size-6" /> | ||
<p className=" text-nowrap tracking-tighter font-medium"> | ||
{item.title} | ||
</p> | ||
</Link> | ||
</motion.div> | ||
))} | ||
</motion.div> | ||
)} | ||
</AnimatePresence> | ||
<button | ||
type="button" | ||
onClick={() => setOpen(!open)} | ||
className="size-16 rounded-full border border-neutral-400/20 bg-neutral-50 dark:bg-neutral-800 flex items-center justify-center" | ||
> | ||
<ListCollapseIcon className="size-7 text-neutral-500 dark:text-neutral-400" /> | ||
</button> | ||
</div> | ||
); | ||
}; | ||
|
||
const FloatingDockDesktop = ({ | ||
items, | ||
className, | ||
}: { | ||
items: { title: string; Icon: LucideIcon; href: string }[]; | ||
className?: string; | ||
}) => { | ||
const mouseX = useMotionValue(Number.POSITIVE_INFINITY); | ||
return ( | ||
<motion.div | ||
onMouseMove={(e) => mouseX.set(e.pageX)} | ||
onMouseLeave={() => mouseX.set(Number.POSITIVE_INFINITY)} | ||
className={cn( | ||
"mx-auto hidden md:flex h-14 gap-2 items-end rounded-full bg-neutral-50 dark:bg-neutral-900 px-2 pb-2 border border-neutral-500/20", | ||
className, | ||
)} | ||
> | ||
{items.map((item) => ( | ||
<IconContainer mouseX={mouseX} key={item.title} {...item} /> | ||
))} | ||
</motion.div> | ||
); | ||
}; | ||
|
||
function IconContainer({ | ||
mouseX, | ||
title, | ||
Icon, | ||
href, | ||
}: Readonly<{ | ||
mouseX: MotionValue; | ||
title: string; | ||
Icon: LucideIcon; | ||
href: string; | ||
}>) { | ||
const pathname = usePathname(); | ||
const [isActive, setIsActive] = useState(false); | ||
|
||
useEffect(() => { | ||
if (href === "/") { | ||
setIsActive(href === pathname); | ||
} else { | ||
setIsActive(pathname.includes(href)); | ||
} | ||
}, [pathname, href]); | ||
|
||
const ref = useRef<HTMLDivElement>(null); | ||
|
||
const distance = useTransform(mouseX, (val) => { | ||
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 }; | ||
|
||
return val - bounds.x - bounds.width / 2; | ||
}); | ||
|
||
const widthTransform = useTransform(distance, [-150, 0, 150], [60, 80, 60]); | ||
const heightTransform = useTransform(distance, [-150, 0, 150], [40, 60, 40]); | ||
|
||
const widthTransformIcon = useTransform( | ||
distance, | ||
[-150, 0, 150], | ||
[20, 40, 20], | ||
); | ||
const heightTransformIcon = useTransform( | ||
distance, | ||
[-150, 0, 150], | ||
[20, 40, 20], | ||
); | ||
|
||
const width = useSpring(widthTransform, { | ||
mass: 0.1, | ||
stiffness: 300, | ||
damping: 12, | ||
}); | ||
const height = useSpring(heightTransform, { | ||
mass: 0.1, | ||
stiffness: 300, | ||
damping: 12, | ||
}); | ||
|
||
const widthIcon = useSpring(widthTransformIcon, { | ||
mass: 0.1, | ||
stiffness: 300, | ||
damping: 12, | ||
}); | ||
const heightIcon = useSpring(heightTransformIcon, { | ||
mass: 0.1, | ||
stiffness: 300, | ||
damping: 12, | ||
}); | ||
|
||
const [hovered, setHovered] = useState(false); | ||
|
||
return ( | ||
<Link href={href}> | ||
<motion.div | ||
ref={ref} | ||
style={{ width, height }} | ||
onMouseEnter={() => setHovered(true)} | ||
onMouseLeave={() => setHovered(false)} | ||
className={cn( | ||
"aspect-square rounded-full border border-neutral-400/20 backdrop-blur-2xl flex items-center justify-center relative", | ||
isActive | ||
? "bg-neutral-800 dark:bg-neutral-100" | ||
: "bg-neutral-100 dark:bg-neutral-800", | ||
)} | ||
> | ||
<AnimatePresence> | ||
{hovered && ( | ||
// ------ Tooltip ------ // | ||
<motion.div | ||
initial={{ opacity: 0, y: 10, x: "-50%" }} | ||
animate={{ opacity: 1, y: 0, x: "-50%" }} | ||
exit={{ opacity: 0, y: 2, x: "-50%" }} | ||
className="px-2 py-0.5 whitespace-pre rounded-md bg-neutral-100 dark:bg-neutral-800 dark:text-white border border-neutral-500/20 text-neutral-700 absolute left-1/2 -translate-x-1/2 -top-8 w-fit text-xs" | ||
> | ||
{title} | ||
</motion.div> | ||
)} | ||
</AnimatePresence> | ||
<motion.div | ||
style={{ width: widthIcon, height: heightIcon }} | ||
className={cn( | ||
"flex items-center justify-center", | ||
isActive | ||
? "*:text-neutral-100 *:dark:text-neutral-800" | ||
: "*:text-neutral-800 *:dark:text-neutral-100", | ||
)} | ||
> | ||
<Icon className="size-6" /> | ||
</motion.div> | ||
</motion.div> | ||
</Link> | ||
); | ||
} |
Oops, something went wrong.