From 9632958e41366f42dc3318881f0e0414cf987a08 Mon Sep 17 00:00:00 2001 From: MOH Date: Mon, 9 Mar 2026 08:39:49 +0100 Subject: [PATCH] Refine home hero and header experience --- app/globals.css | 30 ++++----- components/layout/home-hero.tsx | 103 +++++++++++++++++++++--------- components/layout/site-header.tsx | 36 +++++------ components/layout/site-hero.tsx | 34 +++++++--- components/layout/site-logo.tsx | 41 ++++++++++-- messages/ar.json | 2 +- 6 files changed, 166 insertions(+), 80 deletions(-) diff --git a/app/globals.css b/app/globals.css index b600302..42648a9 100644 --- a/app/globals.css +++ b/app/globals.css @@ -368,34 +368,32 @@ html[lang="ar"] { .hero-title-accent { background-image: linear-gradient( - 120deg, - hsl(var(--hero-ink)) 0%, - hsl(var(--hero-ink)) 24%, - hsl(var(--primary) / 0.58) 52%, - hsl(var(--hero-right) / 0.34) 78%, - hsl(var(--hero-ink)) 100% + 135deg, + hsl(var(--primary)) 0%, + color-mix(in srgb, hsl(var(--primary)) 82%, hsl(var(--foreground))) 100% ); - background-size: 220% 220%; background-clip: text; -webkit-background-clip: text; color: transparent; - animation: hero-title-shift 8s ease-in-out infinite alternate; } .hero-title-accent-soft { + color: hsl(var(--hero-ink)); + } + + .dark .hero-title-accent { background-image: linear-gradient( - 120deg, - hsl(var(--hero-ink)) 0%, - hsl(var(--hero-ink)) 34%, - hsl(var(--hero-left) / 0.26) 62%, - hsl(var(--primary) / 0.24) 82%, - hsl(var(--hero-ink)) 100% + 135deg, + hsl(var(--primary)) 0%, + color-mix(in srgb, hsl(var(--primary)) 74%, hsl(var(--foreground))) 100% ); - background-size: 220% 220%; background-clip: text; -webkit-background-clip: text; color: transparent; - animation: hero-title-shift 10s ease-in-out infinite alternate; + } + + .dark .hero-title-accent-soft { + color: hsl(var(--hero-ink)); } .hero-scroll-link { diff --git a/components/layout/home-hero.tsx b/components/layout/home-hero.tsx index a0d65c2..ac76b46 100644 --- a/components/layout/home-hero.tsx +++ b/components/layout/home-hero.tsx @@ -1,7 +1,8 @@ +import { motion, useReducedMotion } from "framer-motion"; + import { HeroContentMotion, HeroMotionItem, - HeroScrollLink, HeroShell, HeroTitle, } from "@/components/layout/site-hero"; @@ -19,36 +20,83 @@ export function HomeHero({ title, description, }: HomeHeroProps) { + const reducedMotion = useReducedMotion(); const titleLines = title.split("\n").filter(Boolean); const isArabic = locale === "ar"; - const lines = titleLines.map((line, index) => ({ - text: line, - tone: isArabic - ? index === 0 - ? "accent" - : "default" - : index === titleLines.length - 1 - ? "accent" - : index === 0 - ? "soft" - : "default", - align: isArabic - ? "center" - : index === 1 - ? "end" - : "start", - })) as { - text: string; - tone: "default" | "accent" | "soft"; - align: "start" | "center" | "end"; - }[]; + const lines = isArabic + ? [ + { + text: titleLines[0] ?? "", + align: "center" as const, + segments: [ + { text: "مطوّر ", tone: "accent" as const }, + { text: "واجهات", tone: "soft" as const }, + { text: " ويب", tone: "accent" as const }, + ], + }, + { + text: titleLines[1] ?? "", + align: "center" as const, + segments: [ + { text: "ومصمّم ", tone: "soft" as const }, + { text: "جرافيك", tone: "accent" as const }, + ], + }, + ] + : [ + { + text: titleLines[0] ?? "", + tone: "soft" as const, + align: "start" as const, + }, + { + text: titleLines[1] ?? "", + tone: "accent" as const, + align: "end" as const, + }, + { + text: titleLines[2] ?? "", + segments: [ + { text: "& ", tone: "accent" as const }, + { text: "designer", tone: "soft" as const }, + ], + align: "start" as const, + }, + ]; return ( -

- {kicker} +

+ {kicker} +

@@ -57,13 +105,6 @@ export function HomeHero({ {description}

- - -
); diff --git a/components/layout/site-header.tsx b/components/layout/site-header.tsx index c7c0650..ff3d8ac 100644 --- a/components/layout/site-header.tsx +++ b/components/layout/site-header.tsx @@ -128,6 +128,7 @@ export function SiteHeader({ }: SiteHeaderProps) { const [isOpen, setIsOpen] = useState(false); const [isScrolled, setIsScrolled] = useState(false); + const [logoReplayKey, setLogoReplayKey] = useState(0); const pathname = usePathname(); const locale = useLocale(); const t = useTranslations("navigation"); @@ -137,7 +138,7 @@ export function SiteHeader({ ? "مــــــطـــــــــــــور ويــــــــــب | فــــــــــــريــــــلانــــــســــــر" : "Webdesigner Bremen | Freelancer"; 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 backdrop-blur-chrome"; const desktopControlButtonClassName = "h-9 w-9 rounded-pill border border-transparent p-0 text-foreground/80 hover:bg-accent hover:text-foreground"; const mobileControlButtonClassName = @@ -158,7 +159,7 @@ export function SiteHeader({ return (
- +
setLogoReplayKey((value) => value + 1)} > - - - + @@ -298,7 +294,7 @@ export function SiteHeader({ onClick={() => setIsOpen((open) => !open)} variant="ghost" 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 shadow-panel backdrop-blur-chrome hover:bg-accent lg:hidden" + 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" aria-label={isOpen ? t("closeMenu") : t("openMenu")} > - + = { }; const lineAlignClasses: Record = { - start: "self-start text-start", + start: "self-center text-center lg:self-start lg:text-start", center: "self-center text-center", - end: "self-end text-end", + end: "self-center text-center lg:self-end lg:text-end", }; export function HeroShell({ @@ -97,7 +103,7 @@ export function HeroShell({
@@ -183,9 +189,9 @@ export function HeroTitle({ "text-balance font-semibold text-[hsl(var(--hero-ink))]", variant === "home" ? isArabic - ? "mt-6 flex w-full max-w-[16rem] flex-col gap-y-1 text-[clamp(3rem,11vw,5.9rem)] leading-[1.06] tracking-[-0.03em] md:max-w-[22rem] lg:max-w-[32rem]" - : "mt-6 flex w-full max-w-[16rem] flex-col gap-y-1 text-[clamp(3.1rem,11vw,6.3rem)] leading-[0.94] tracking-[-0.06em] md:max-w-[22rem] lg:max-w-[32rem]" - : "mt-4 flex w-full max-w-[16rem] flex-col gap-y-1 text-[clamp(2.8rem,10vw,4.9rem)] leading-[0.96] tracking-[-0.06em] md:max-w-[22rem] lg:max-w-[32rem]", + ? "mt-6 flex w-full max-w-[20rem] flex-col gap-y-1 text-[clamp(3rem,11vw,5.9rem)] leading-[1.06] tracking-[-0.03em] sm:max-w-[24rem] md:max-w-[30rem] lg:max-w-[32rem]" + : "mt-6 flex w-full max-w-[20rem] flex-col gap-y-1 text-[clamp(3.1rem,11vw,6.3rem)] leading-[0.94] tracking-[-0.06em] sm:max-w-[24rem] md:max-w-[30rem] lg:max-w-[32rem]" + : "mt-4 flex w-full max-w-[20rem] flex-col gap-y-1 text-[clamp(2.8rem,10vw,4.9rem)] leading-[0.96] tracking-[-0.06em] sm:max-w-[24rem] md:max-w-[28rem] lg:max-w-[32rem]", className, )} > @@ -194,11 +200,23 @@ export function HeroTitle({ key={`${index}-${line.text}`} className={cn( "hero-title-line", - toneClasses[line.tone ?? "default"], lineAlignClasses[line.align ?? "center"], )} > - {line.text} + {line.segments?.length + ? line.segments.map((segment, segmentIndex) => ( + + {segment.text} + + )) + : ( + + {line.text} + + )} ))} diff --git a/components/layout/site-logo.tsx b/components/layout/site-logo.tsx index ff72bbc..7ef571b 100644 --- a/components/layout/site-logo.tsx +++ b/components/layout/site-logo.tsx @@ -1,6 +1,6 @@ "use client"; -import { motion } from "framer-motion"; +import { motion, useAnimationControls, useReducedMotion } from "framer-motion"; import Image from "next/image"; import { useTheme } from "next-themes"; import { useEffect, useState } from "react"; @@ -13,6 +13,7 @@ type SiteLogoProps = { alt: string; priority?: boolean; className?: string; + replayKey?: number; }; export function SiteLogo({ @@ -21,14 +22,46 @@ export function SiteLogo({ alt, priority = false, className, + replayKey = 0, }: SiteLogoProps) { const { theme, resolvedTheme } = useTheme(); const [mounted, setMounted] = useState(false); + const controls = useAnimationControls(); + const reducedMotion = useReducedMotion(); useEffect(() => { setMounted(true); }, []); + useEffect(() => { + if (reducedMotion) { + return; + } + + controls.set({ + opacity: 0, + x: -42, + scale: 0.88, + rotate: -360, + filter: "blur(8px)", + }); + + void controls.start({ + opacity: 1, + x: 0, + scale: 1, + rotate: 0, + filter: "blur(0px)", + transition: { + x: { duration: 0.78, ease: [0.22, 1, 0.36, 1] }, + opacity: { duration: 0.46 }, + rotate: { duration: 0.56, ease: "easeOut" }, + scale: { duration: 0.72, ease: [0.22, 1, 0.36, 1] }, + filter: { duration: 0.42, ease: [0.22, 1, 0.36, 1] }, + }, + }); + }, [controls, reducedMotion, replayKey]); + const activeTheme = theme === "system" ? resolvedTheme : theme; const isDark = mounted && activeTheme === "dark"; const fallbackSrc = isDark ? "/logos/dark-primary.svg" : "/logos/light-primary.svg"; @@ -37,11 +70,10 @@ export function SiteLogo({ return (