Refine localized header branding
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
||||||
import { LayoutDashboard, Menu, X } from "lucide-react";
|
import { LayoutDashboard, Menu, X } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useLocale, useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { Container } from "@/components/layout/container";
|
import { Container } from "@/components/layout/container";
|
||||||
import { LocaleToggle } from "@/components/layout/locale-toggle";
|
import { LocaleToggle } from "@/components/layout/locale-toggle";
|
||||||
@@ -127,10 +127,15 @@ export function SiteHeader({
|
|||||||
darkLogoUrl,
|
darkLogoUrl,
|
||||||
}: SiteHeaderProps) {
|
}: SiteHeaderProps) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const t = useTranslations("navigation");
|
const t = useTranslations("navigation");
|
||||||
|
const reducedMotion = useReducedMotion();
|
||||||
const isArabic = locale === "ar";
|
const isArabic = locale === "ar";
|
||||||
|
const brandSubtitle = isArabic
|
||||||
|
? "مــــــطـــــــــــــور ويــــــــــب | فــــــــــــريــــــلانــــــســــــر"
|
||||||
|
: "Webdesigner Bremen | Freelancer";
|
||||||
const desktopControlRailClassName =
|
const desktopControlRailClassName =
|
||||||
"inline-flex items-center gap-1 rounded-pill border border-border/70 bg-background/80 p-1 shadow-panel backdrop-blur-chrome";
|
"inline-flex items-center gap-1 rounded-pill border border-border/70 bg-background/80 p-1 shadow-panel backdrop-blur-chrome";
|
||||||
const desktopControlButtonClassName =
|
const desktopControlButtonClassName =
|
||||||
@@ -138,19 +143,40 @@ export function SiteHeader({
|
|||||||
const mobileControlButtonClassName =
|
const mobileControlButtonClassName =
|
||||||
"h-10 w-10 rounded-pill border border-border/70 bg-background text-foreground/80 shadow-xs hover:bg-accent hover:text-foreground";
|
"h-10 w-10 rounded-pill border border-border/70 bg-background text-foreground/80 shadow-xs hover:bg-accent hover:text-foreground";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setIsScrolled(window.scrollY > 24);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleScroll();
|
||||||
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="fixed inset-x-0 top-3 z-40 bg-transparent">
|
<header className="fixed inset-x-0 top-3 z-40 bg-transparent">
|
||||||
<Container className="max-w-[88rem]">
|
<Container className="max-w-[88rem]">
|
||||||
<div className="relative grid min-h-[4.5rem] grid-cols-[auto_1fr_auto] items-center gap-3 lg:min-h-[5rem]">
|
<div className="relative grid min-h-[4.5rem] grid-cols-[auto_1fr_auto] items-center gap-3 lg:min-h-[5rem]">
|
||||||
<div className="col-start-1 flex items-center justify-self-start">
|
<motion.div
|
||||||
|
initial={false}
|
||||||
|
animate={isScrolled ? { opacity: 0, x: -28 } : { opacity: 1, x: 0 }}
|
||||||
|
transition={reducedMotion ? { duration: 0 } : { duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
className={cn(
|
||||||
|
"col-start-1 flex items-center justify-self-start",
|
||||||
|
isScrolled ? "lg:pointer-events-none lg:invisible" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
href={getLocalizedPath(locale)}
|
href={getLocalizedPath(locale)}
|
||||||
className="inline-flex items-center gap-2.5 rounded-pill px-1 py-1 sm:gap-3 lg:py-2"
|
className="inline-flex items-center gap-2.5 rounded-pill px-1 py-1 sm:gap-3 lg:py-2"
|
||||||
>
|
>
|
||||||
<motion.span
|
<motion.span
|
||||||
initial={{ opacity: 0, x: -18, scale: 0.82, rotate: -14, filter: "blur(10px)" }}
|
initial={reducedMotion ? false : isArabic ? { opacity: 0, x: 18, scale: 0.82, rotate: 14, filter: "blur(10px)" } : { opacity: 0, x: -18, scale: 0.82, rotate: -14, filter: "blur(10px)" }}
|
||||||
animate={{ opacity: 1, x: 0, scale: 1, rotate: 0, filter: "blur(0px)" }}
|
animate={{ opacity: 1, x: 0, scale: 1, rotate: 0, filter: "blur(0px)" }}
|
||||||
transition={{ type: "spring", stiffness: 240, damping: 20, mass: 0.9 }}
|
transition={reducedMotion ? { duration: 0 } : { duration: 0.32, ease: [0.22, 1, 0.36, 1] }}
|
||||||
className="relative inline-flex items-center justify-center [transform-style:preserve-3d]"
|
className="relative inline-flex items-center justify-center [transform-style:preserve-3d]"
|
||||||
>
|
>
|
||||||
<SiteLogo
|
<SiteLogo
|
||||||
@@ -162,16 +188,28 @@ export function SiteHeader({
|
|||||||
/>
|
/>
|
||||||
</motion.span>
|
</motion.span>
|
||||||
<motion.span
|
<motion.span
|
||||||
initial="hidden"
|
initial={isArabic ? false : "hidden"}
|
||||||
animate="visible"
|
animate="visible"
|
||||||
variants={brandShellVariants}
|
variants={reducedMotion || isArabic ? undefined : brandShellVariants}
|
||||||
className="flex flex-col justify-center"
|
className="flex flex-col justify-center"
|
||||||
>
|
>
|
||||||
|
{isArabic ? (
|
||||||
|
<motion.span
|
||||||
|
initial={reducedMotion ? false : { opacity: 0, x: 16, filter: "blur(8px)" }}
|
||||||
|
animate={{ opacity: 1, x: 0, filter: "blur(0px)" }}
|
||||||
|
transition={reducedMotion ? { duration: 0 } : { duration: 0.42, delay: 0.12, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
className="flex flex-col text-[1.28rem] font-black leading-[1.2]"
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
<span className="text-[hsl(var(--hero-ink))] dark:text-foreground">مــــحـــمــد</span>
|
||||||
|
<span className="text-primary">فــــرواتـــي</span>
|
||||||
|
</motion.span>
|
||||||
|
) : (
|
||||||
<span className="flex flex-wrap items-center text-[1.28rem] font-black leading-[1.2]">
|
<span className="flex flex-wrap items-center text-[1.28rem] font-black leading-[1.2]">
|
||||||
{brandTitleCharacters.map(({ character, tone }, index) => (
|
{brandTitleCharacters.map(({ character, tone }, index) => (
|
||||||
<motion.span
|
<motion.span
|
||||||
key={`${character}-${index}`}
|
key={`${character}-${index}`}
|
||||||
variants={brandCharacterVariants}
|
variants={reducedMotion ? undefined : brandCharacterVariants}
|
||||||
className={cn(
|
className={cn(
|
||||||
character === " " ? "mr-[0.11em] w-[0.11em]" : "mr-[0.005em] inline-block",
|
character === " " ? "mr-[0.11em] w-[0.11em]" : "mr-[0.005em] inline-block",
|
||||||
tone === "base" ? "text-[hsl(var(--hero-ink))] dark:text-foreground" : "",
|
tone === "base" ? "text-[hsl(var(--hero-ink))] dark:text-foreground" : "",
|
||||||
@@ -182,25 +220,36 @@ export function SiteHeader({
|
|||||||
</motion.span>
|
</motion.span>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
<motion.span
|
<motion.span
|
||||||
initial={{ opacity: 0, y: 20, clipPath: "inset(0 0 100% 0)", filter: "blur(10px)" }}
|
initial={
|
||||||
animate={{ opacity: 1, y: 0, clipPath: "inset(0 0 0% 0)", filter: "blur(0px)" }}
|
reducedMotion
|
||||||
transition={{ duration: 0.7, delay: 0.34, ease: [0.22, 1, 0.36, 1] }}
|
? false
|
||||||
className="-mt-0.5 text-[0.65rem] font-light leading-[1.2] tracking-[0.008em] text-foreground/78"
|
: 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] }}
|
||||||
|
className={cn(
|
||||||
|
"-mt-0.5 text-[0.65rem] font-light leading-[1.2] tracking-[0.008em] text-foreground/78",
|
||||||
|
isArabic ? "text-right" : "",
|
||||||
|
)}
|
||||||
|
dir={isArabic ? "rtl" : undefined}
|
||||||
>
|
>
|
||||||
Webdesigner Bremen | Freelancer
|
{brandSubtitle}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
</motion.span>
|
</motion.span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="pointer-events-none absolute left-1/2 top-1/2 hidden -translate-x-1/2 -translate-y-1/2 lg:flex">
|
<div className="col-start-2 hidden justify-center lg:flex">
|
||||||
<motion.nav
|
<motion.nav
|
||||||
initial={{ opacity: 0, y: -8 }}
|
initial={false}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={isScrolled ? { y: -1, scale: 0.985 } : { y: 0, scale: 1 }}
|
||||||
transition={{ duration: 0.28, ease: "easeOut" }}
|
transition={reducedMotion ? { duration: 0 } : { duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-auto inline-flex items-center gap-1 rounded-pill border border-border/70 bg-background/80 p-1.5 shadow-panel backdrop-blur-chrome",
|
"inline-flex items-center gap-1 rounded-pill border border-border/70 bg-background/80 p-1.5 shadow-panel 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]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -208,13 +257,16 @@ export function SiteHeader({
|
|||||||
</motion.nav>
|
</motion.nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden items-center justify-end lg:flex">
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -8 }}
|
initial={false}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={isScrolled ? { opacity: 0, x: 28 } : { opacity: 1, x: 0 }}
|
||||||
transition={{ duration: 0.28, ease: "easeOut", delay: 0.03 }}
|
transition={reducedMotion ? { duration: 0 } : { duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
|
||||||
className={desktopControlRailClassName}
|
className={cn(
|
||||||
|
"col-start-3 hidden items-center justify-end lg:flex",
|
||||||
|
isScrolled ? "pointer-events-none invisible" : "",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
|
<div className={desktopControlRailClassName}>
|
||||||
<LocaleToggle
|
<LocaleToggle
|
||||||
locale={locale}
|
locale={locale}
|
||||||
className={desktopControlButtonClassName}
|
className={desktopControlButtonClassName}
|
||||||
@@ -237,8 +289,8 @@ export function SiteHeader({
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user