Polish header entrance motion
This commit is contained in:
@@ -24,47 +24,24 @@ const navItems = [
|
|||||||
{ key: "contact", path: "/contact" },
|
{ key: "contact", path: "/contact" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const brandTitleCharacters = [
|
|
||||||
...Array.from("MOH").map((character) => ({ character, tone: "base" as const })),
|
|
||||||
{ character: " ", tone: "space" as const },
|
|
||||||
...Array.from("FARAWATI").map((character) => ({ character, tone: "accent" as const })),
|
|
||||||
];
|
|
||||||
const brandShellVariants = {
|
|
||||||
hidden: {},
|
|
||||||
visible: {
|
|
||||||
transition: {
|
|
||||||
staggerChildren: 0.035,
|
|
||||||
delayChildren: 0.18,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
const brandCharacterVariants = {
|
|
||||||
hidden: {
|
|
||||||
opacity: 0,
|
|
||||||
y: 18,
|
|
||||||
rotateX: -80,
|
|
||||||
filter: "blur(8px)",
|
|
||||||
},
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
rotateX: 0,
|
|
||||||
filter: "blur(0px)",
|
|
||||||
transition: {
|
|
||||||
type: "spring" as const,
|
|
||||||
stiffness: 280,
|
|
||||||
damping: 20,
|
|
||||||
mass: 0.8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type SiteHeaderProps = {
|
type SiteHeaderProps = {
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
lightLogoUrl?: string | null;
|
lightLogoUrl?: string | null;
|
||||||
darkLogoUrl?: string | null;
|
darkLogoUrl?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getDirectionalReveal(direction: "rtl" | "ltr") {
|
||||||
|
return direction === "rtl"
|
||||||
|
? {
|
||||||
|
initial: { opacity: 0, x: -16, clipPath: "inset(0 0 0 100%)", filter: "blur(10px)" },
|
||||||
|
animate: { opacity: 1, x: 0, y: 0, clipPath: "inset(0 0 0 0)", filter: "blur(0px)" },
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
initial: { opacity: 0, x: 16, clipPath: "inset(0 100% 0 0)", filter: "blur(10px)" },
|
||||||
|
animate: { opacity: 1, x: 0, y: 0, clipPath: "inset(0 0 0 0)", filter: "blur(0px)" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function isNavItemActive(currentPath: string, itemPath: string) {
|
function isNavItemActive(currentPath: string, itemPath: string) {
|
||||||
if (itemPath === "/") {
|
if (itemPath === "/") {
|
||||||
return currentPath === "/";
|
return currentPath === "/";
|
||||||
@@ -134,6 +111,29 @@ export function SiteHeader({
|
|||||||
const t = useTranslations("navigation");
|
const t = useTranslations("navigation");
|
||||||
const reducedMotion = useReducedMotion();
|
const reducedMotion = useReducedMotion();
|
||||||
const isArabic = locale === "ar";
|
const isArabic = locale === "ar";
|
||||||
|
const languageDirection = isArabic ? "rtl" : "ltr";
|
||||||
|
const titleMotion = getDirectionalReveal(languageDirection);
|
||||||
|
const subtitleMotion = getDirectionalReveal(languageDirection);
|
||||||
|
const navEntranceInitial = reducedMotion ? false : { opacity: 0, y: -14, scale: 0.985, filter: "blur(8px)" };
|
||||||
|
const navEntranceVisible = { opacity: 1, y: 0, scale: 1, filter: "blur(0px)" };
|
||||||
|
const controlEntranceInitial = reducedMotion
|
||||||
|
? false
|
||||||
|
: {
|
||||||
|
opacity: 0,
|
||||||
|
x: languageDirection === "rtl" ? -18 : 18,
|
||||||
|
scale: 0.98,
|
||||||
|
filter: "blur(8px)",
|
||||||
|
};
|
||||||
|
const controlEntranceVisible = { opacity: 1, x: 0, scale: 1, filter: "blur(0px)" };
|
||||||
|
const mobileMenuInitial = reducedMotion
|
||||||
|
? false
|
||||||
|
: {
|
||||||
|
opacity: 0,
|
||||||
|
x: languageDirection === "rtl" ? -14 : 14,
|
||||||
|
scale: 0.94,
|
||||||
|
filter: "blur(8px)",
|
||||||
|
};
|
||||||
|
const mobileMenuVisible = { opacity: 1, x: 0, scale: 1, filter: "blur(0px)" };
|
||||||
const brandSubtitle = isArabic
|
const brandSubtitle = isArabic
|
||||||
? "مــــــطـــــــــــــور ويــــــــــب | فــــــــــــريــــــلانــــــســــــر"
|
? "مــــــطـــــــــــــور ويــــــــــب | فــــــــــــريــــــلانــــــســــــر"
|
||||||
: "Webdesigner Bremen | Freelancer";
|
: "Webdesigner Bremen | Freelancer";
|
||||||
@@ -182,51 +182,39 @@ export function SiteHeader({
|
|||||||
priority
|
priority
|
||||||
className="relative h-10 opacity-95"
|
className="relative h-10 opacity-95"
|
||||||
replayKey={logoReplayKey}
|
replayKey={logoReplayKey}
|
||||||
|
direction={languageDirection}
|
||||||
/>
|
/>
|
||||||
<motion.span
|
<span
|
||||||
initial={isArabic ? false : "hidden"}
|
key={`${locale}-${logoReplayKey}`}
|
||||||
animate="visible"
|
|
||||||
variants={reducedMotion || isArabic ? undefined : brandShellVariants}
|
|
||||||
className="flex flex-col justify-center"
|
className="flex flex-col justify-center"
|
||||||
>
|
>
|
||||||
{isArabic ? (
|
|
||||||
<motion.span
|
<motion.span
|
||||||
initial={reducedMotion ? false : { opacity: 0, x: 16, filter: "blur(8px)" }}
|
initial={reducedMotion ? false : titleMotion.initial}
|
||||||
animate={{ opacity: 1, x: 0, filter: "blur(0px)" }}
|
animate={titleMotion.animate}
|
||||||
transition={reducedMotion ? { duration: 0 } : { duration: 0.42, delay: 0.12, ease: [0.22, 1, 0.36, 1] }}
|
transition={reducedMotion ? { duration: 0 } : { duration: 0.7, delay: 0.12, ease: [0.22, 1, 0.36, 1] }}
|
||||||
className="text-[1.28rem] font-black leading-[1.2] whitespace-nowrap"
|
className={cn(
|
||||||
dir="rtl"
|
"text-[1.28rem] font-black leading-[1.2] whitespace-nowrap",
|
||||||
|
isArabic ? "text-right" : "",
|
||||||
|
)}
|
||||||
|
dir={isArabic ? "rtl" : undefined}
|
||||||
>
|
>
|
||||||
|
{isArabic ? (
|
||||||
|
<>
|
||||||
<span className="text-[hsl(var(--hero-ink))] dark:text-foreground">مــــحـــمــد</span>
|
<span className="text-[hsl(var(--hero-ink))] dark:text-foreground">مــــحـــمــد</span>
|
||||||
{" "}
|
{" "}
|
||||||
<span className="text-primary">فــــرواتـــي</span>
|
<span className="text-primary">فــــرواتـــي</span>
|
||||||
</motion.span>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex flex-wrap items-center text-[1.28rem] font-black leading-[1.2]">
|
<>
|
||||||
{brandTitleCharacters.map(({ character, tone }, index) => (
|
<span className="text-[hsl(var(--hero-ink))] dark:text-foreground">MOH</span>
|
||||||
<motion.span
|
{" "}
|
||||||
key={`${character}-${index}`}
|
<span className="text-primary">FARAWATI</span>
|
||||||
variants={reducedMotion ? undefined : brandCharacterVariants}
|
</>
|
||||||
className={cn(
|
|
||||||
character === " " ? "mr-[0.11em] w-[0.11em]" : "mr-[0.005em] inline-block",
|
|
||||||
tone === "base" ? "text-[hsl(var(--hero-ink))] dark:text-foreground" : "",
|
|
||||||
tone === "accent" ? "text-primary" : "",
|
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
{character === " " ? "\u00A0" : character}
|
|
||||||
</motion.span>
|
</motion.span>
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<motion.span
|
<motion.span
|
||||||
initial={
|
initial={reducedMotion ? false : subtitleMotion.initial}
|
||||||
reducedMotion
|
animate={subtitleMotion.animate}
|
||||||
? false
|
|
||||||
: isArabic
|
|
||||||
? { opacity: 0, x: -16, clipPath: "inset(0 0 0 100%)", filter: "blur(10px)" }
|
|
||||||
: { opacity: 0, x: 16, clipPath: "inset(0 100% 0 0)", filter: "blur(10px)" }
|
|
||||||
}
|
|
||||||
animate={{ opacity: 1, x: 0, y: 0, clipPath: "inset(0 0 0% 0)", filter: "blur(0px)" }}
|
|
||||||
transition={reducedMotion ? { duration: 0 } : { duration: 0.7, delay: 0.34, ease: [0.22, 1, 0.36, 1] }}
|
transition={reducedMotion ? { duration: 0 } : { duration: 0.7, delay: 0.34, ease: [0.22, 1, 0.36, 1] }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"-mt-0.5 text-[0.65rem] font-light leading-[1.2] tracking-[0.008em] text-foreground/78",
|
"-mt-0.5 text-[0.65rem] font-light leading-[1.2] tracking-[0.008em] text-foreground/78",
|
||||||
@@ -236,15 +224,19 @@ export function SiteHeader({
|
|||||||
>
|
>
|
||||||
{brandSubtitle}
|
{brandSubtitle}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
</motion.span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="col-start-2 hidden justify-center lg:flex">
|
<div className="col-start-2 hidden justify-center lg:flex">
|
||||||
<motion.nav
|
<motion.nav
|
||||||
initial={false}
|
initial={navEntranceInitial}
|
||||||
animate={isScrolled ? { y: -1, scale: 0.985 } : { y: 0, scale: 1 }}
|
animate={
|
||||||
transition={reducedMotion ? { duration: 0 } : { duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
|
isScrolled
|
||||||
|
? { opacity: 1, y: -1, scale: 0.985, filter: "blur(0px)" }
|
||||||
|
: navEntranceVisible
|
||||||
|
}
|
||||||
|
transition={reducedMotion ? { duration: 0 } : { duration: 0.56, delay: 0.22, ease: [0.22, 1, 0.36, 1] }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center gap-1 rounded-pill border border-border/70 bg-background/80 p-1.5 backdrop-blur-chrome",
|
"inline-flex items-center gap-1 rounded-pill border border-border/70 bg-background/80 p-1.5 backdrop-blur-chrome",
|
||||||
isArabic ? "w-auto max-w-none" : "min-w-[27rem] max-w-[42rem]",
|
isArabic ? "w-auto max-w-none" : "min-w-[27rem] max-w-[42rem]",
|
||||||
@@ -255,9 +247,13 @@ export function SiteHeader({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={false}
|
initial={controlEntranceInitial}
|
||||||
animate={isScrolled ? { opacity: 0, x: 28 } : { opacity: 1, x: 0 }}
|
animate={
|
||||||
transition={reducedMotion ? { duration: 0 } : { duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
|
isScrolled
|
||||||
|
? { opacity: 0, x: languageDirection === "rtl" ? -28 : 28, scale: 0.98, filter: "blur(4px)" }
|
||||||
|
: controlEntranceVisible
|
||||||
|
}
|
||||||
|
transition={reducedMotion ? { duration: 0 } : { duration: 0.56, delay: 0.3, ease: [0.22, 1, 0.36, 1] }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"col-start-3 hidden items-center justify-end lg:flex",
|
"col-start-3 hidden items-center justify-end lg:flex",
|
||||||
isScrolled ? "pointer-events-none invisible" : "",
|
isScrolled ? "pointer-events-none invisible" : "",
|
||||||
@@ -289,12 +285,18 @@ export function SiteHeader({
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={mobileMenuInitial}
|
||||||
|
animate={mobileMenuVisible}
|
||||||
|
transition={reducedMotion ? { duration: 0 } : { duration: 0.52, delay: 0.2, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
className="col-start-3 ml-auto justify-self-end lg:hidden"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsOpen((open) => !open)}
|
onClick={() => setIsOpen((open) => !open)}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="col-start-3 ml-auto h-11 w-11 justify-self-end rounded-pill border border-border/70 bg-background/80 text-foreground backdrop-blur-chrome hover:bg-accent lg:hidden"
|
className="h-11 w-11 rounded-pill border border-border/70 bg-background/80 text-foreground backdrop-blur-chrome hover:bg-accent"
|
||||||
aria-label={isOpen ? t("closeMenu") : t("openMenu")}
|
aria-label={isOpen ? t("closeMenu") : t("openMenu")}
|
||||||
>
|
>
|
||||||
<motion.span
|
<motion.span
|
||||||
@@ -305,6 +307,7 @@ export function SiteHeader({
|
|||||||
{isOpen ? <X className="h-4 w-4" /> : <Menu className="h-4 w-4" />}
|
{isOpen ? <X className="h-4 w-4" /> : <Menu className="h-4 w-4" />}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
</Button>
|
</Button>
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type SiteLogoProps = {
|
|||||||
priority?: boolean;
|
priority?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
replayKey?: number;
|
replayKey?: number;
|
||||||
|
direction?: "rtl" | "ltr";
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SiteLogo({
|
export function SiteLogo({
|
||||||
@@ -23,11 +24,15 @@ export function SiteLogo({
|
|||||||
priority = false,
|
priority = false,
|
||||||
className,
|
className,
|
||||||
replayKey = 0,
|
replayKey = 0,
|
||||||
|
direction = "ltr",
|
||||||
}: SiteLogoProps) {
|
}: SiteLogoProps) {
|
||||||
const { theme, resolvedTheme } = useTheme();
|
const { theme, resolvedTheme } = useTheme();
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const controls = useAnimationControls();
|
const containerControls = useAnimationControls();
|
||||||
|
const spinControls = useAnimationControls();
|
||||||
const reducedMotion = useReducedMotion();
|
const reducedMotion = useReducedMotion();
|
||||||
|
const directionalOffset = direction === "rtl" ? 42 : -42;
|
||||||
|
const spinStart = direction === "rtl" ? -360 : 360;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
@@ -38,29 +43,35 @@ export function SiteLogo({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
controls.set({
|
containerControls.set({
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
x: -42,
|
x: directionalOffset,
|
||||||
scale: 0.88,
|
scale: 0.88,
|
||||||
rotate: -360,
|
|
||||||
filter: "blur(8px)",
|
filter: "blur(8px)",
|
||||||
});
|
});
|
||||||
|
spinControls.set({ rotate: 0 });
|
||||||
|
|
||||||
void controls.start({
|
void containerControls.start({
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
x: 0,
|
x: 0,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
rotate: 0,
|
|
||||||
filter: "blur(0px)",
|
filter: "blur(0px)",
|
||||||
transition: {
|
transition: {
|
||||||
x: { duration: 0.78, ease: [0.22, 1, 0.36, 1] },
|
x: { duration: 0.78, ease: [0.22, 1, 0.36, 1] },
|
||||||
opacity: { duration: 0.46 },
|
opacity: { duration: 0.46 },
|
||||||
rotate: { duration: 0.56, ease: "easeOut" },
|
|
||||||
scale: { duration: 0.72, ease: [0.22, 1, 0.36, 1] },
|
scale: { duration: 0.72, ease: [0.22, 1, 0.36, 1] },
|
||||||
filter: { duration: 0.42, ease: [0.22, 1, 0.36, 1] },
|
filter: { duration: 0.42, ease: [0.22, 1, 0.36, 1] },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [controls, reducedMotion, replayKey]);
|
|
||||||
|
void spinControls.start({
|
||||||
|
rotate: spinStart,
|
||||||
|
transition: {
|
||||||
|
duration: 0.78,
|
||||||
|
ease: [0.22, 1, 0.36, 1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [containerControls, directionalOffset, reducedMotion, replayKey, spinControls, spinStart]);
|
||||||
|
|
||||||
const activeTheme = theme === "system" ? resolvedTheme : theme;
|
const activeTheme = theme === "system" ? resolvedTheme : theme;
|
||||||
const isDark = mounted && activeTheme === "dark";
|
const isDark = mounted && activeTheme === "dark";
|
||||||
@@ -69,25 +80,32 @@ export function SiteLogo({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.span
|
<motion.span
|
||||||
className="block [transform-style:preserve-3d]"
|
className="block transform-gpu [transform-style:preserve-3d]"
|
||||||
initial={reducedMotion ? false : undefined}
|
initial={reducedMotion ? false : undefined}
|
||||||
animate={reducedMotion ? undefined : controls}
|
animate={reducedMotion ? undefined : containerControls}
|
||||||
whileHover={{ scale: 1.05, y: -2, rotate: -2, filter: "drop-shadow(0 16px 28px rgba(15,23,42,0.14))" }}
|
transition={{ duration: 0.32, ease: [0.22, 1, 0.36, 1] }}
|
||||||
whileTap={{ scale: 0.9, rotate: 6, y: 1, filter: "drop-shadow(0 8px 18px rgba(15,23,42,0.18))" }}
|
whileHover={reducedMotion ? undefined : { scale: 1.03, y: -1.5, rotate: -1.25 }}
|
||||||
|
whileTap={reducedMotion ? undefined : { scale: 0.97, rotate: 1.5, y: 0.5 }}
|
||||||
|
>
|
||||||
|
<motion.span
|
||||||
|
className="block origin-center"
|
||||||
|
initial={reducedMotion ? false : { rotate: 0 }}
|
||||||
|
animate={reducedMotion ? undefined : spinControls}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={src}
|
src={src}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
width={360}
|
width={360}
|
||||||
height={92}
|
height={92}
|
||||||
sizes="(min-width: 1280px) 360px, (min-width: 1024px) 320px, (min-width: 640px) 280px, 240px"
|
sizes="240px"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-9 w-auto object-contain object-left drop-shadow-[0_10px_24px_rgba(15,23,42,0.12)] transition-[filter,opacity,transform] duration-300 ease-out sm:h-10 lg:h-11 xl:h-12",
|
"h-10 w-auto object-contain object-left drop-shadow-[0_10px_24px_rgba(15,23,42,0.12)] transition-opacity duration-300 ease-out",
|
||||||
mounted ? "opacity-100" : "opacity-0",
|
mounted ? "opacity-100" : "opacity-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
priority={priority}
|
priority={priority}
|
||||||
/>
|
/>
|
||||||
</motion.span>
|
</motion.span>
|
||||||
|
</motion.span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user