Initial commit

This commit is contained in:
2026-02-09 18:13:28 +02:00
commit 0d608affae
656 changed files with 77411 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
"use client";
import { cls } from "@/lib/utils";
interface HamburgerButtonProps {
isActive: boolean;
onClick: () => void;
className?: string;
activeBarClassName?: string;
inactiveBarClassName?: string;
ariaControls?: string;
}
const HamburgerButton = ({
isActive,
onClick,
className = "",
activeBarClassName = "bg-background",
inactiveBarClassName = "bg-foreground",
ariaControls = "navigation-menu",
}: HamburgerButtonProps) => {
return (
<button
onClick={onClick}
className={cls(
"pointer-events-auto cursor-pointer bg-transparent border-none flex justify-center items-center h-9 w-[var(--height-9)] aspect-square relative",
className
)}
aria-label={isActive ? "Close menu" : "Open menu"}
aria-expanded={isActive}
aria-controls={ariaControls}
>
<span
aria-hidden="true"
className={cls(
"transition-all duration-700 ease-[cubic-bezier(0.5,0.5,0,1)] w-[40%] h-0.25 absolute",
isActive
? `${activeBarClassName} translate-y-0 rotate-45`
: `${inactiveBarClassName} -translate-y-1 hover:translate-y-1`
)}
/>
<span
aria-hidden="true"
className={cls(
"transition-all duration-700 ease-[cubic-bezier(0.5,0.5,0,1)] w-[40%] h-0.25 absolute",
isActive
? `${activeBarClassName} translate-y-0 -rotate-45`
: `${inactiveBarClassName} translate-y-1 hover:-translate-y-1`
)}
/>
</button>
);
};
HamburgerButton.displayName = "HamburgerButton";
export default HamburgerButton;

View File

@@ -0,0 +1,66 @@
"use client";
// import Image from "next/image";
import { cls } from "@/lib/utils";
import { useButtonClick } from "@/components/button/useButtonClick";
interface LogoProps {
// logoSrc?: string;
// logoAlt?: string;
brandName?: string;
// className?: string;
// imageClassName?: string;
textClassName?: string;
onClick?: () => void;
href?: string;
}
const Logo = ({
// logoSrc,
// logoAlt = "",
brandName = "Webild",
// className = "",
// imageClassName = "",
textClassName = "",
onClick,
href,
}: LogoProps) => {
const handleClick = useButtonClick(href, onClick);
// if (logoSrc) {
// return (
// <div className={cls("relative h-[var(--text-xl)] w-auto", className)}>
// <Image
// src={logoSrc}
// alt={logoAlt}
// width={100}
// height={24}
// className={cls("h-full w-auto object-contain", imageClassName)}
// unoptimized={logoSrc.startsWith('http') || logoSrc.startsWith('//')}
// />
// </div>
// );
// }
const isClickable = onClick || href;
if (isClickable) {
return (
<button onClick={handleClick} className="cursor-pointer">
<h2 className={cls("text-xl font-medium text-foreground", textClassName)}>
{brandName}
</h2>
</button>
);
}
return (
<h2 className={cls("text-xl font-medium text-foreground", textClassName)}>
{brandName}
</h2>
);
};
Logo.displayName = "Logo";
export default Logo;

View File

@@ -0,0 +1,81 @@
"use client";
import Button from "../button/Button";
import ButtonTextUnderline from "../button/ButtonTextUnderline";
import Logo from "./Logo";
import { NavItem } from "@/types/navigation";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { ButtonConfig } from "@/types/button";
interface NavbarLayoutFloatingInlineProps {
navItems: NavItem[];
// logoSrc?: string;
// logoAlt?: string;
brandName?: string;
button: ButtonConfig;
className?: string;
navItemClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
logoOnClick?: () => void;
logoHref?: string;
}
const NavbarLayoutFloatingInline = ({
navItems,
// logoSrc,
// logoAlt = "",
brandName = "Webild",
button,
className = "",
navItemClassName = "",
buttonClassName = "",
buttonTextClassName = "",
logoOnClick,
logoHref,
}: NavbarLayoutFloatingInlineProps) => {
const theme = useTheme();
return (
<nav
role="navigation"
aria-label="Main navigation"
className="fixed z-[100] top-6 w-full transition-all duration-500 ease-in-out"
>
<div className={cls(
"w-content-width mx-auto",
"flex items-center justify-between",
"card rounded-theme",
"p-3 pl-6 h-fit relative",
className
)}>
<Logo brandName={brandName} onClick={logoOnClick} href={logoHref} />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 hidden md:flex gap-6 items-center">
{navItems.map((item, index) => (
<ButtonTextUnderline
key={index}
text={item.name}
href={item.id}
className={cls("!text-base", navItemClassName)}
/>
))}
</div>
<Button
{...getButtonProps(
button,
0,
theme.defaultButtonVariant,
buttonClassName,
buttonTextClassName
)}
/>
</div>
</nav>
);
};
export default NavbarLayoutFloatingInline;

View File

@@ -0,0 +1,94 @@
"use client";
import ExpandingMenu from "../expandingMenu/ExpandingMenu";
import Button from "../../button/Button";
import Logo from "../Logo";
import { useScrollDetection } from "./useScrollDetection";
import { useMenuAnimation } from "./useMenuAnimation";
import { useResponsive } from "./useResponsive";
import type { NavItem } from "@/types/navigation";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { ButtonConfig } from "@/types/button";
interface NavbarLayoutFloatingOverlayProps {
navItems: NavItem[];
// logoSrc?: string;
// logoAlt?: string;
className?: string;
brandName?: string;
button: ButtonConfig;
buttonClassName?: string;
buttonTextClassName?: string;
logoOnClick?: () => void;
logoHref?: string;
}
const NavbarLayoutFloatingOverlay = ({
navItems,
// logoSrc,
// logoAlt = "",
className = "",
brandName = "Webild",
button,
buttonClassName = "",
buttonTextClassName = "",
logoOnClick,
logoHref,
}: NavbarLayoutFloatingOverlayProps) => {
const theme = useTheme();
const isScrolled = useScrollDetection(50);
const { menuOpen, buttonZIndex, handleMenuToggle } =
useMenuAnimation();
const isMobile = useResponsive(768);
return (
<nav
role="navigation"
aria-label="Main navigation"
className="fixed z-[100] top-6 w-full transition-all duration-500 ease-in-out"
>
<div
className={cls(
"w-content-width mx-auto",
"flex items-center justify-between",
"card rounded-theme backdrop-blur-xs",
"px-6 md:pr-3",
className
)}
style={{
height: "calc(var(--vw-0_75) + var(--vw-0_75) + var(--height-9))",
}}
>
<Logo brandName={brandName} onClick={logoOnClick} href={logoHref} />
<div
className="flex items-center transition-all duration-500 ease-in-out"
style={{ paddingRight: "calc(var(--height-9) + var(--vw-0_75))" }}
>
{!isMobile && (
<div className="hidden md:flex">
<Button
{...getButtonProps(
button,
0,
theme.defaultButtonVariant,
cls(buttonZIndex, buttonClassName),
buttonTextClassName
)}
/>
</div>
)}
<ExpandingMenu
isOpen={menuOpen}
onToggle={handleMenuToggle}
navItems={navItems}
isScrolled={isScrolled}
/>
</div>
</div>
</nav>
);
};
export default NavbarLayoutFloatingOverlay;

View File

@@ -0,0 +1,40 @@
import { useState, useCallback, useEffect } from 'react';
export const useMenuAnimation = () => {
const [menuOpen, setMenuOpen] = useState(false);
const [buttonZIndex, setButtonZIndex] = useState('z-[100]');
const [animationTimeoutId, setAnimationTimeoutId] = useState<NodeJS.Timeout | null>(null);
const handleMenuToggle = useCallback(() => {
const isOpening = !menuOpen;
setMenuOpen(prev => !prev);
if (animationTimeoutId) {
clearTimeout(animationTimeoutId);
}
if (isOpening) {
setButtonZIndex('z-0');
} else {
const timeoutId = setTimeout(() => {
setButtonZIndex('z-[100]');
}, 800);
setAnimationTimeoutId(timeoutId);
}
}, [menuOpen, animationTimeoutId]);
useEffect(() => {
return () => {
if (animationTimeoutId) {
clearTimeout(animationTimeoutId);
}
};
}, [animationTimeoutId]);
return {
menuOpen,
buttonZIndex,
handleMenuToggle,
setMenuOpen
};
};

View File

@@ -0,0 +1,18 @@
import { useState, useEffect } from 'react';
import { throttle } from '@/utils/throttle';
export const useResponsive = (breakpoint: number = 768) => {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const handleResize = throttle(() => {
setIsMobile(window.innerWidth < breakpoint);
}, 150);
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [breakpoint]);
return isMobile;
};

View File

@@ -0,0 +1,18 @@
import { useState, useEffect } from 'react';
import { throttle } from '@/utils/throttle';
export const useScrollDetection = (threshold: number = 50) => {
const [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
const handleScroll = throttle(() => {
setIsScrolled(window.scrollY > threshold);
}, 100);
handleScroll();
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, [threshold]);
return isScrolled;
};

View File

@@ -0,0 +1,132 @@
"use client";
import { useState, useCallback } from "react";
import MobileMenu from "../mobileMenu/MobileMenu";
import Button from "@/components/button/Button";
import ButtonTextUnderline from "@/components/button/ButtonTextUnderline";
import Logo from "../Logo";
import { Plus } from "lucide-react";
import { NavbarProps } from "@/types/navigation";
import { useScrollState } from "./useScrollState";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { ButtonConfig } from "@/types/button";
const SCROLL_THRESHOLD = 50;
interface NavbarStyleAppleProps extends NavbarProps {
button?: ButtonConfig;
buttonClassName?: string;
buttonTextClassName?: string;
}
const NavbarStyleApple = ({
navItems,
// logoSrc,
// logoAlt = "",
brandName = "Webild",
logoOnClick,
logoHref,
button,
buttonClassName = "",
buttonTextClassName = "",
}: NavbarStyleAppleProps) => {
const isScrolled = useScrollState(SCROLL_THRESHOLD);
const [menuOpen, setMenuOpen] = useState(false);
const theme = useTheme();
const handleMenuToggle = useCallback(() => {
setMenuOpen((prev) => !prev);
}, []);
const handleMobileNavClick = useCallback(() => {
setMenuOpen(false);
}, []);
return (
<nav
className={cls(
"fixed z-[1000] top-0 left-0 w-full transition-all duration-500 ease-in-out",
isScrolled
? "bg-background/80 backdrop-blur-sm h-15"
: "bg-background/0 backdrop-blur-0 h-20"
)}
>
<div className="flex items-center justify-between h-full w-content-width mx-auto">
<div className="flex items-center transition-all duration-500 ease-in-out">
<Logo brandName={brandName} onClick={logoOnClick} href={logoHref} />
</div>
<div
className="hidden md:flex items-center gap-6 transition-all duration-500 ease-in-out"
role="navigation"
>
{navItems.map((item, index) => (
<ButtonTextUnderline
key={index}
text={item.name}
href={item.id}
className="!text-base"
/>
))}
{button && (
<Button
{...getButtonProps(
button,
0,
theme.defaultButtonVariant,
buttonClassName,
buttonTextClassName
)}
/>
)}
</div>
<button
className="flex md:hidden shrink-0 h-8 aspect-square rounded-theme bg-foreground items-center justify-center cursor-pointer"
onClick={handleMenuToggle}
aria-label="Toggle menu"
aria-expanded={menuOpen}
aria-controls="mobile-menu"
>
<Plus
className={cls(
"w-1/2 h-1/2 text-background transition-transform duration-300",
menuOpen ? "rotate-45" : "rotate-0"
)}
strokeWidth={1.5}
aria-hidden="true"
/>
</button>
</div>
<MobileMenu
menuOpen={menuOpen}
onMenuToggle={handleMenuToggle}
navItems={navItems}
onNavClick={handleMobileNavClick}
>
{button && (
<Button
{...getButtonProps(
{
...button,
onClick: () => {
button.onClick?.();
setMenuOpen(false);
},
},
0,
theme.defaultButtonVariant,
cls("w-full", buttonClassName),
buttonTextClassName
)}
/>
)}
</MobileMenu>
</nav>
);
};
export default NavbarStyleApple;

View File

@@ -0,0 +1,28 @@
import { useState, useEffect, useRef } from 'react';
export const useScrollState = (threshold: number = 50) => {
const [isScrolled, setIsScrolled] = useState(false);
const rafRef = useRef<number | undefined>(undefined);
useEffect(() => {
const handleScroll = () => {
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
rafRef.current = requestAnimationFrame(() => {
setIsScrolled(window.scrollY > threshold);
});
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => {
window.removeEventListener('scroll', handleScroll);
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
};
}, [threshold]);
return isScrolled;
};

View File

@@ -0,0 +1,40 @@
/* Overlay - active state */
[data-navigation-status="active"] .centered-nav__overlay {
opacity: 0.15;
visibility: visible;
}
/* Content expand - grid animation */
.centered-nav__content {
grid-template-rows: 0fr;
}
[data-navigation-status="active"] .centered-nav__content {
grid-template-rows: 1fr;
}
/* Inner container - height hack for grid animation */
.centered-nav__inner {
height: 10000%;
}
/* Link content slide up animation */
.centered-nav__link-content {
transition: transform 0.6s cubic-bezier(0.65, 0, 0, 1);
transform: translateY(150%);
transition-delay: inherit;
}
[data-navigation-status="active"] .centered-nav__link-content {
transform: translateY(0);
}
/* Separator width animation */
.centered-nav__separator {
width: 0;
transition: width 0.6s cubic-bezier(0.65, 0, 0, 1);
}
[data-navigation-status="active"] .centered-nav__separator {
width: 100%;
}

View File

@@ -0,0 +1,161 @@
"use client";
import { useState, useEffect, useCallback, Fragment } from "react";
import Logo from "../Logo";
import HamburgerButton from "../HamburgerButton";
import Button from "@/components/button/Button";
import { NavItem } from "@/types/navigation";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useButtonClick } from "@/components/button/useButtonClick";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { ButtonConfig } from "@/types/button";
import "./NavbarStyleCentered.css";
import { ArrowUpRight } from "lucide-react";
interface NavLinkProps {
item: NavItem;
index: number;
onClose: () => void;
}
const NavLink = ({ item, index, onClose }: NavLinkProps) => {
const handleClick = useButtonClick(item.id, onClose);
return (
<li
className="group m-0 p-0 list-none overflow-clip"
style={{ transitionDelay: `${index * 0.05}s` }}
>
<button
type="button"
className="centered-nav__link relative flex justify-between items-center no-underline w-full text-left bg-transparent border-none cursor-pointer"
onClick={handleClick}
>
<div className="centered-nav__link-content flex justify-between items-center gap-3 w-full">
<p className="m-0 text-xl md:text-2xl text-foreground truncate group-hover:ml-3 transition-[margin] duration-300">{item.name}</p>
<ArrowUpRight className="h-[var(--text-xl)] md:h-[var(--text-2xl)] w-auto text-foreground group-hover:rotate-45 group-hover:mr-3 transition-all duration-300" strokeWidth={1.5} />
</div>
</button>
</li>
);
};
interface NavbarStyleCenteredProps {
navItems: NavItem[];
button: ButtonConfig;
// logoSrc?: string;
// logoAlt?: string;
brandName?: string;
className?: string;
logoOnClick?: () => void;
logoHref?: string;
}
const NavbarStyleCentered = ({
navItems,
button,
// logoSrc,
// logoAlt = "",
brandName = "Webild",
className = "",
logoOnClick,
logoHref,
}: NavbarStyleCenteredProps) => {
const [isActive, setIsActive] = useState(false);
const theme = useTheme();
const getButtonConfigProps = () => {
if (theme.defaultButtonVariant === "hover-bubble") {
return { bgClassName: "w-full" };
}
if (theme.defaultButtonVariant === "icon-arrow") {
return { className: "justify-between" };
}
return {};
};
const handleToggle = useCallback(() => {
setIsActive((prev) => !prev);
}, []);
const handleClose = useCallback(() => {
setIsActive(false);
}, []);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && isActive) {
setIsActive(false);
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isActive]);
return (
<nav
role="navigation"
aria-label="Main navigation"
data-navigation-status={isActive ? "active" : "not-active"}
className="fixed inset-0 z-[1000] pointer-events-none"
>
<div
className="centered-nav__overlay absolute inset-0 bg-foreground pointer-events-auto opacity-0 invisible transition-all duration-700 ease-[cubic-bezier(0.5,0.5,0,1)]"
onClick={handleClose}
/>
<div className={cls("pointer-events-auto absolute top-6 left-1/2 -translate-x-1/2 w-content-width md:top-8 md:w-35 flex flex-col justify-start items-stretch rounded-theme-capped", className)} >
<div className="absolute! inset-0 card backdrop-blur-xs rounded-theme-capped" />
<div className="relative z-10 flex justify-between items-center py-3 px-6">
<Logo
brandName={brandName}
onClick={logoOnClick}
href={logoHref}
/>
<HamburgerButton
isActive={isActive}
onClick={handleToggle}
activeBarClassName="bg-foreground"
inactiveBarClassName="bg-foreground"
/>
</div>
<div className="centered-nav__content relative overflow-hidden rounded-b-theme-capped grid transition-[grid-template-rows] duration-600 ease-[cubic-bezier(0.625,0.05,0,1)]">
<div className="centered-nav__inner flex flex-col justify-start items-center w-full relative overflow-hidden gap-6">
<div className="w-full px-6" >
<ul className="relative w-full card p-6 rounded-theme-capped flex flex-col gap-3 justify-start items-stretch m-0 list-none">
{navItems.map((item, index) => (
<Fragment key={item.id}>
<NavLink item={item} index={index} onClose={handleClose} />
{index < navItems.length - 1 && <div className="centered-nav__separator h-px bg-accent/50" />}
</Fragment>
))}
</ul>
</div>
<div className="w-full px-6 pb-6">
<Button
{...getButtonProps(
{
...button,
onClick: () => {
button.onClick?.();
handleClose();
},
props: { ...button.props, ...getButtonConfigProps() }
},
0,
theme.defaultButtonVariant,
"w-full"
)}
/>
</div>
</div>
</div>
</div>
</nav>
);
};
NavbarStyleCentered.displayName = "NavbarStyleCentered";
export default NavbarStyleCentered;

View File

@@ -0,0 +1,58 @@
/* Tile clip-path animation */
.navbar-fullscreen__tile {
clip-path: polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%);
transition: clip-path 1s cubic-bezier(.9, 0, .1, 1);
}
[data-navigation-status="active"] .navbar-fullscreen__tile {
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
}
/* Link initial state and animation */
.navbar-fullscreen__link {
transform: translateY(100%) rotate(5deg);
transition: transform 0.75s cubic-bezier(.7, 0, .3, 1);
}
/* Link staggered delays (closing) */
.navbar-fullscreen__li:nth-child(1) .navbar-fullscreen__link { transition-delay: 0.2s; }
.navbar-fullscreen__li:nth-child(2) .navbar-fullscreen__link { transition-delay: 0.15s; }
.navbar-fullscreen__li:nth-child(3) .navbar-fullscreen__link { transition-delay: 0.1s; }
.navbar-fullscreen__li:nth-child(4) .navbar-fullscreen__link { transition-delay: 0.05s; }
.navbar-fullscreen__li:nth-child(5) .navbar-fullscreen__link { transition-delay: 0s; }
/* Link animation (Navigation Open) */
[data-navigation-status="active"] .navbar-fullscreen__link {
transform: translateY(0%) rotate(0.001deg);
}
/* Link staggered delays (opening) */
[data-navigation-status="active"] .navbar-fullscreen__li:nth-child(1) .navbar-fullscreen__link { transition-delay: 0.3s; }
[data-navigation-status="active"] .navbar-fullscreen__li:nth-child(2) .navbar-fullscreen__link { transition-delay: 0.35s; }
[data-navigation-status="active"] .navbar-fullscreen__li:nth-child(3) .navbar-fullscreen__link { transition-delay: 0.4s; }
[data-navigation-status="active"] .navbar-fullscreen__li:nth-child(4) .navbar-fullscreen__link { transition-delay: 0.45s; }
[data-navigation-status="active"] .navbar-fullscreen__li:nth-child(5) .navbar-fullscreen__link { transition-delay: 0.5s; }
/* Link text duplicate effect */
.navbar-fullscreen__link-text {
text-shadow: 0 1.1em 0;
transition: transform 0.5s cubic-bezier(.7, 0, .3, 1);
transform: translateY(0%) rotate(0.001deg);
}
.navbar-fullscreen__link:hover .navbar-fullscreen__link-text {
transform: translateY(-100%) rotate(0.001deg);
}
/* Hover dim effect on siblings */
.navbar-fullscreen__li {
transition: opacity 0.5s cubic-bezier(.7, 0, .3, 1);
}
.navbar-fullscreen__ul:has(.navbar-fullscreen__li:hover) .navbar-fullscreen__li {
opacity: 0.15;
}
.navbar-fullscreen__ul:has(.navbar-fullscreen__li:hover) .navbar-fullscreen__li:hover {
opacity: 1;
}

View File

@@ -0,0 +1,141 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import Logo from "../Logo";
import HamburgerButton from "../HamburgerButton";
import Button from "@/components/button/Button";
import { NavItem } from "@/types/navigation";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useButtonClick } from "@/components/button/useButtonClick";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { ButtonConfig } from "@/types/button";
import "./NavbarStyleFullscreen.css";
interface NavLinkProps {
item: NavItem;
onClose: () => void;
}
const NavLink = ({ item, onClose }: NavLinkProps) => {
const handleClick = useButtonClick(item.id, onClose);
return (
<button
type="button"
className="navbar-fullscreen__link text-background font-normal leading-[1.15] no-underline text-9xl bg-transparent border-none cursor-pointer"
onClick={handleClick}
>
<span className="navbar-fullscreen__link-text block relative">{item.name}</span>
</button>
);
};
interface NavbarStyleFullscreenProps {
navItems: NavItem[];
// logoSrc?: string;
// logoAlt?: string;
brandName?: string;
bottomLeftText?: string;
bottomRightText?: string;
topBarClassName?: string;
logoOnClick?: () => void;
logoHref?: string;
button?: ButtonConfig;
buttonClassName?: string;
buttonTextClassName?: string;
}
const NavbarStyleFullscreen = ({
navItems,
// logoSrc,
// logoAlt = "",
brandName = "Webild",
bottomLeftText = "Global Community",
bottomRightText = "hello@example.com",
topBarClassName = "",
logoOnClick,
logoHref,
button,
buttonClassName = "",
buttonTextClassName = "",
}: NavbarStyleFullscreenProps) => {
const [isActive, setIsActive] = useState(false);
const theme = useTheme();
const handleToggle = useCallback(() => {
setIsActive((prev) => !prev);
}, []);
const handleClose = useCallback(() => {
setIsActive(false);
}, []);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && isActive) {
setIsActive(false);
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isActive]);
return (
<nav
role="navigation"
aria-label="Main navigation"
data-navigation-status={isActive ? "active" : "not-active"}
className="fixed inset-0 z-[100] pointer-events-none"
>
<div className={cls(
"absolute z-1 w-content-width left-1/2 -translate-x-1/2 top-6 flex justify-between items-center pointer-events-auto",
topBarClassName
)}>
<Logo
brandName={brandName}
textClassName={`transition-colors duration-700 ease-[cubic-bezier(0.5,0.5,0,1)] ${isActive ? "text-background" : "text-foreground"}`}
onClick={logoOnClick}
href={logoHref}
/>
<div className="flex items-center gap-3">
{button && (
<Button
{...getButtonProps(
button,
0,
theme.defaultButtonVariant,
buttonClassName,
buttonTextClassName
)}
/>
)}
<HamburgerButton isActive={isActive} onClick={handleToggle} />
</div>
</div>
<div
id="navigation-menu"
className="navbar-fullscreen__tile pointer-events-auto bg-foreground backdrop-blur absolute inset-0 flex flex-col justify-center items-center"
>
<ul className="navbar-fullscreen__ul flex flex-col items-center m-0 p-0 list-none">
{navItems.map((item) => (
<li key={item.id} className="navbar-fullscreen__li flex justify-center items-center m-0 p-0 relative overflow-hidden">
<NavLink item={item} onClose={handleClose} />
</li>
))}
</ul>
<div className="absolute bottom-0 w-content-width left-1/2 -translate-x-1/2 flex justify-between items-center py-10">
<p className="text-background/50 mb-0 text-base relative">{bottomLeftText}</p>
<p className="text-background/50 mb-0 text-base relative">{bottomRightText}</p>
</div>
</div>
</nav>
);
};
NavbarStyleFullscreen.displayName = "NavbarStyleFullscreen";
export default NavbarStyleFullscreen;

View File

@@ -0,0 +1,149 @@
"use client";
import { useCallback } from 'react';
import { useResponsiveMenuWidth } from './useResponsiveMenuWidth';
import { useButtonClick } from '@/components/button/useButtonClick';
interface NavItem {
id: string;
name: string;
}
interface ExpandingMenuProps {
isOpen: boolean;
onToggle: () => void;
navItems: NavItem[];
isScrolled?: boolean;
}
const ExpandingMenu = ({
isOpen,
onToggle,
navItems,
isScrolled = false
}: ExpandingMenuProps) => {
const { isMounted, menuWidth } = useResponsiveMenuWidth();
const handleNavClick = useCallback(() => {
onToggle();
}, [onToggle]);
return (
<div
className={`
rounded-theme-capped absolute top-[calc(var(--vw-0_75)-1px)] right-3
transition-[top] duration-500 ease-in-out
${isScrolled ? '' : ''}
${isOpen ? 'pointer-events-auto' : 'pointer-events-none'}
`}>
<div
aria-hidden="true"
className={`
primary-button
backdrop-blur-xs
transition-all duration-700 ease-[cubic-bezier(0.5,0.5,0,1)]
bg-foreground rounded-theme-capped absolute top-0 right-0
${isOpen
? 'w-full h-full'
: 'h-9 w-[var(--height-9)]'
}
`}
/>
<div className={`
relative p-6 flex flex-col gap-6
transition-all duration-500 ease-[cubic-bezier(0.5,0.5,0,1)]
origin-[100%_0]
${isOpen
? 'scale-100 opacity-100 visible pointer-events-auto'
: 'scale-[0.15] opacity-0 invisible pointer-events-none'
}
`}
style={{
transition: 'all 0.5s cubic-bezier(0.5, 0.5, 0, 1), transform 0.7s cubic-bezier(0.5, 0.5, 0, 1)',
width: isMounted ? menuWidth : 'var(--width-20)'
}}
>
<p className="text-xl text-background" aria-hidden="true">Menu</p>
<ul
role="menu"
className="relative list-none flex flex-col gap-3 m-0 p-0"
>
{navItems.map((item) => {
const MenuButton = () => {
const handleClick = useButtonClick(item.id, handleNavClick);
return (
<button
aria-label={`Navigate to ${item.name}`}
className={`
text-background flex justify-between items-center
no-underline bg-none border-none cursor-pointer w-full
font-inherit group
`}
onClick={handleClick}
>
<span className="text-base">
{item.name}
</span>
<div className="bg-current rounded-theme-capped shrink-0 h-2 aspect-square" />
</button>
);
};
return (
<li
key={item.id}
role="menuitem"
className="m-0 p-0 list-none"
>
<MenuButton />
</li>
);
})}
</ul>
</div>
<button
aria-label={isOpen ? 'Close menu' : 'Open menu'}
aria-expanded={isOpen}
aria-controls="navigation-menu"
className={`
transition-transform duration-700 ease-[cubic-bezier(0.5,0.5,0,1)]
pointer-events-auto cursor-pointer rounded-theme-capped
flex justify-center items-center
h-9 w-[var(--height-9)] aspect-square absolute top-0 right-0
bg-transparent border-none
${isOpen
? '-translate-x-3 translate-y-3'
: 'translate-x-0 translate-y-0'
}
`}
onClick={onToggle}
>
<span
aria-hidden="true"
className={`
transition-transform duration-700 ease-[cubic-bezier(0.5,0.5,0,1)]
bg-background w-[40%] h-0.25 absolute
${isOpen
? 'translate-y-0 rotate-45'
: '-translate-y-1 hover:translate-y-1'
}
`} />
<span
aria-hidden="true"
className={`
transition-transform duration-700 ease-[cubic-bezier(0.5,0.5,0,1)]
bg-background w-[40%] h-0.25 absolute
${isOpen
? 'translate-y-0 -rotate-45'
: 'translate-y-1 hover:-translate-y-1'
}
`} />
</button>
</div>
);
};
export default ExpandingMenu;

View File

@@ -0,0 +1,23 @@
import { useState, useEffect } from 'react';
export const useResponsiveMenuWidth = () => {
const [isMounted, setIsMounted] = useState(false);
const [menuWidth, setMenuWidth] = useState('var(--width-20)');
useEffect(() => {
setIsMounted(true);
const handleResize = () => {
setMenuWidth(
window.innerWidth >= 768
? 'var(--width-20)'
: 'calc(var(--width-80) - var(--vw-0_75) * 2)'
);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return { isMounted, menuWidth };
};

View File

@@ -0,0 +1,79 @@
"use client";
import { Fragment } from 'react';
import { ArrowRight, Plus } from 'lucide-react';
import { NavItem } from '@/types/navigation';
import { useMenuAnimation } from './useMenuAnimation';
import { useButtonClick } from '@/components/button/useButtonClick';
interface MobileMenuProps {
menuOpen: boolean;
onMenuToggle: () => void;
navItems: NavItem[];
onNavClick: (id: string) => void;
children?: React.ReactNode;
}
const MobileMenu = ({
menuOpen,
onMenuToggle,
navItems,
onNavClick,
children
}: MobileMenuProps) => {
const menuRef = useMenuAnimation(menuOpen);
return (
<div
id="mobile-menu"
className="md:hidden z-10 fixed top-3 left-1/2 -translate-x-1/2 h-fit rounded-theme-capped card p-6 flex flex-col gap-6 opacity-0"
style={{ width: 'calc(100vw - var(--vw-0_75) * 2)' }}
ref={menuRef}
role="navigation"
aria-label="Mobile navigation menu"
>
<div className="w-full flex justify-between items-center">
<p className="text-xl text-foreground">Menu</p>
<button
className="shrink-0 h-8 aspect-square rounded-theme bg-foreground flex items-center justify-center cursor-pointer"
onClick={onMenuToggle}
aria-label="Close menu"
>
<Plus className="w-1/2 h-1/2 text-background rotate-45" strokeWidth={1.5} aria-hidden="true" />
</button>
</div>
<div className="flex flex-col gap-4">
{navItems.map((item, index) => {
const NavButton = () => {
const handleClick = useButtonClick(item.id, () => onNavClick(item.id));
return (
<button
className="w-full h-fit flex justify-between items-center cursor-pointer"
onClick={handleClick}
aria-label={`Navigate to ${item.name}`}
>
<p className="text-base font-medium">{item.name}</p>
<ArrowRight strokeWidth={1.5} className="h-[var(--text-base)] w-auto text-foreground" aria-hidden="true" />
</button>
);
};
return (
<Fragment key={index}>
<NavButton />
{index < navItems.length - 1 && (
<div className="w-full h-px bg-gradient-to-r from-transparent via-foreground/20 to-transparent" />
)}
</Fragment>
);
})}
</div>
{children && (
<div className="flex gap-3 items-center">{children}</div>
)}
</div>
);
};
export default MobileMenu;

View File

@@ -0,0 +1,40 @@
import { useRef, useEffect } from 'react';
import { gsap } from 'gsap';
export const useMenuAnimation = (menuOpen: boolean) => {
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (menuRef.current) {
if (menuOpen) {
gsap.to(menuRef.current, {
y: "0%",
opacity: 1,
pointerEvents: "auto",
duration: 0.8,
ease: "power3.out"
});
} else {
gsap.to(menuRef.current, {
y: "-135%",
opacity: 1,
pointerEvents: "none",
duration: 0.8,
ease: "power3.inOut"
});
}
}
}, [menuOpen]);
useEffect(() => {
if (menuRef.current) {
gsap.set(menuRef.current, {
y: "-135%",
opacity: 1,
pointerEvents: "none"
});
}
}, []);
return menuRef;
};