Refactor site hero and localized branding UI
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
type HeroMotionBackdropProps = {
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
export function HeroMotionBackdrop({ compact = false }: HeroMotionBackdropProps) {
|
||||
return (
|
||||
<div className="hero-sheet pointer-events-none absolute inset-0 overflow-hidden">
|
||||
<div className="hero-sheet-base" />
|
||||
<motion.div
|
||||
animate={{
|
||||
x: compact ? [0, 18, -10, 0] : [0, 44, -24, 0],
|
||||
y: compact ? [0, -16, 10, 0] : [0, -34, 18, 0],
|
||||
scale: compact ? [1, 1.03, 0.99, 1] : [1, 1.08, 0.96, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: compact ? 18 : 26,
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="hero-blob hero-blob-left"
|
||||
/>
|
||||
<motion.div
|
||||
animate={{
|
||||
x: compact ? [0, -14, 8, 0] : [0, -36, 18, 0],
|
||||
y: compact ? [0, 10, -8, 0] : [0, 22, -14, 0],
|
||||
scale: compact ? [1, 0.98, 1.04, 1] : [1, 0.94, 1.08, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: compact ? 20 : 30,
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="hero-blob hero-blob-right"
|
||||
/>
|
||||
<motion.div
|
||||
animate={{
|
||||
x: compact ? [0, 12, -8, 0] : [0, 26, -14, 0],
|
||||
y: compact ? [0, -6, 8, 0] : [0, -18, 16, 0],
|
||||
scale: compact ? [1, 1.02, 0.98, 1] : [1, 1.06, 0.97, 1],
|
||||
opacity: compact ? [0.36, 0.5, 0.4, 0.36] : [0.34, 0.54, 0.4, 0.34],
|
||||
}}
|
||||
transition={{
|
||||
duration: compact ? 16 : 24,
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="hero-blob hero-blob-bottom"
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
animate={{
|
||||
opacity: compact ? [0.18, 0.26, 0.2, 0.18] : [0.2, 0.3, 0.22, 0.2],
|
||||
scale: compact ? [1, 1.01, 1, 1] : [1, 1.02, 1, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: compact ? 16 : 22,
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="hero-sheet-glow"
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
animate={{
|
||||
x: compact ? [0, 10, -8, 0] : [0, 18, -14, 0],
|
||||
y: compact ? [0, -8, 6, 0] : [0, -18, 10, 0],
|
||||
rotate: compact ? [0, 2, -1, 0] : [0, 4, -2, 0],
|
||||
opacity: compact ? [0.12, 0.2, 0.14, 0.12] : [0.14, 0.24, 0.16, 0.14],
|
||||
}}
|
||||
transition={{
|
||||
duration: compact ? 20 : 28,
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="hero-sheet-wave"
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
animate={{
|
||||
backgroundPosition: compact
|
||||
? ["0 0, 0 0", "40px 24px, 0 0", "0 0, 0 0"]
|
||||
: ["0 0, 0 0", "64px 40px, 0 0", "0 0, 0 0"],
|
||||
}}
|
||||
transition={{
|
||||
duration: compact ? 20 : 28,
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
ease: "linear",
|
||||
}}
|
||||
className="hero-sheet-noise"
|
||||
/>
|
||||
|
||||
<div className="hero-sheet-veil" />
|
||||
<div className="hero-sheet-fade" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { HeroAtmosphere } from "@/components/layout/hero-atmosphere";
|
||||
import { MotionFade } from "@/components/motion-fade";
|
||||
import { HeroMotionBackdrop } from "@/components/layout/hero-motion-backdrop";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getLocalizedPath } from "@/lib/locale";
|
||||
|
||||
@@ -23,34 +22,60 @@ export function HomeHero({
|
||||
portfolioLabel,
|
||||
contactLabel,
|
||||
}: HomeHeroProps) {
|
||||
const titleLines = title.split("\n").filter(Boolean);
|
||||
const isArabic = locale === "ar";
|
||||
|
||||
return (
|
||||
<section className="relative isolate flex min-h-[85vh] items-center overflow-hidden">
|
||||
<HeroAtmosphere />
|
||||
<section className="hero-surface hero-home relative isolate flex min-h-[88vh] items-center justify-center overflow-hidden">
|
||||
<HeroMotionBackdrop />
|
||||
<div className="hero-header-bridge" />
|
||||
<div className="hero-content-bridge" />
|
||||
<div className="relative z-10 mx-auto flex w-full max-w-[72rem] flex-col items-center justify-center px-4 pb-12 pt-24 text-center sm:px-6 lg:px-8 lg:pb-16 lg:pt-28">
|
||||
<MotionFade className="w-full">
|
||||
<div className="mx-auto max-w-[54rem]">
|
||||
<p className="text-sm font-medium tracking-[0.08em] text-foreground/62">
|
||||
{kicker}
|
||||
</p>
|
||||
<h1 className="mt-6 whitespace-pre-line text-balance text-[3rem] font-semibold leading-[0.92] tracking-[-0.06em] text-foreground sm:text-[4.5rem] lg:text-[6.75rem]">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="mx-auto mt-6 max-w-[30rem] text-sm leading-6 text-foreground/66 sm:text-base">
|
||||
{description}
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||
<Button asChild size="lg" className="px-6">
|
||||
<Link href={getLocalizedPath(locale, "/portfolio")}>
|
||||
{portfolioLabel}
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg" className="px-6">
|
||||
<Link href={getLocalizedPath(locale, "/contact")}>{contactLabel}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mx-auto flex max-w-[46rem] flex-col items-center">
|
||||
<p className="text-base font-medium tracking-[-0.03em] text-[hsl(var(--hero-ink)/0.76)] drop-shadow-[0_10px_24px_rgba(15,23,42,0.16)] sm:text-lg">
|
||||
{kicker}
|
||||
</p>
|
||||
<h1
|
||||
className={
|
||||
isArabic
|
||||
? "mt-5 flex w-full max-w-[40rem] flex-col text-[3rem] font-bold leading-[1.12] tracking-normal text-[hsl(var(--hero-ink))] drop-shadow-[0_18px_40px_rgba(15,23,42,0.16)] sm:text-[4.25rem] lg:max-w-[48rem] lg:text-[5.35rem]"
|
||||
: "mt-4 flex w-full max-w-[36rem] flex-col gap-y-1 text-[3.25rem] font-semibold leading-[0.9] tracking-[-0.085em] text-[hsl(var(--hero-ink))] drop-shadow-[0_18px_40px_rgba(15,23,42,0.22)] sm:max-w-[38rem] sm:text-[4.9rem] lg:max-w-[39rem] lg:gap-y-2 lg:text-[6.15rem]"
|
||||
}
|
||||
>
|
||||
{titleLines.map((line, index) => (
|
||||
<span
|
||||
key={`${locale}-${index}-${line}`}
|
||||
className={
|
||||
isArabic
|
||||
? index === 1
|
||||
? "self-center whitespace-nowrap"
|
||||
: "self-center whitespace-nowrap bg-[linear-gradient(135deg,hsl(var(--hero-ink)),hsl(var(--hero-right)/0.78))] bg-clip-text text-transparent"
|
||||
: index === 1
|
||||
? "self-end"
|
||||
: index === titleLines.length - 1
|
||||
? "self-start bg-[linear-gradient(135deg,hsl(var(--hero-ink)),hsl(var(--hero-right)/0.9))] bg-clip-text text-transparent"
|
||||
: "self-start bg-[linear-gradient(135deg,hsl(var(--hero-ink)),hsl(var(--hero-left)/0.9))] bg-clip-text text-transparent"
|
||||
}
|
||||
>
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</h1>
|
||||
<p className="mx-auto mt-8 max-w-[30rem] whitespace-pre-line text-sm leading-7 text-[hsl(var(--hero-ink)/0.72)] drop-shadow-[0_8px_18px_rgba(15,23,42,0.12)] sm:text-base">
|
||||
{description}
|
||||
</p>
|
||||
<div className="mt-9 flex flex-wrap items-center justify-center gap-3">
|
||||
<Button asChild size="lg" className="min-w-[10.5rem] px-6">
|
||||
<Link href={getLocalizedPath(locale, "/portfolio")}>
|
||||
{portfolioLabel}
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg" className="min-w-[10.5rem] border-white/40 bg-white/35 px-6 backdrop-blur-sm hover:bg-white/55 dark:border-white/14 dark:bg-white/6 dark:hover:bg-white/10">
|
||||
<Link href={getLocalizedPath(locale, "/contact")}>{contactLabel}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</MotionFade>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Languages } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -24,9 +24,16 @@ export function LocaleToggle({ locale, className, showLabel = false }: LocaleTog
|
||||
const locales: AppLocale[] = ["de", "en", "ar"];
|
||||
const currentPath = stripLocalePrefix(pathname);
|
||||
const currentLocale = (["de", "en", "ar"].includes(locale) ? locale : "de") as AppLocale;
|
||||
const availableLocales = locales.filter((targetLocale) => targetLocale !== currentLocale);
|
||||
const contentFontClassName = currentLocale === "ar" ? "font-arabic" : "font-latin";
|
||||
const localeFlags: Record<AppLocale, { src: string; alt: string }> = {
|
||||
de: { src: "/flags/de.svg", alt: "German" },
|
||||
en: { src: "/flags/us.svg", alt: "English" },
|
||||
ar: { src: "/flags/sy.svg", alt: "Arabic" },
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -34,26 +41,41 @@ export function LocaleToggle({ locale, className, showLabel = false }: LocaleTog
|
||||
size={showLabel ? "sm" : "icon"}
|
||||
aria-label={`Locale: ${currentLocale.toUpperCase()}`}
|
||||
className={cn(
|
||||
"rounded-pill border border-transparent text-muted-foreground hover:border-border/70 hover:bg-background/80 hover:text-foreground",
|
||||
"group cursor-pointer rounded-pill border border-transparent text-muted-foreground hover:border-border/70 hover:bg-background/80 hover:text-foreground",
|
||||
showLabel ? "gap-2 px-3" : undefined,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Languages className="h-4 w-4" />
|
||||
{showLabel ? <span className="text-xs font-semibold uppercase tracking-[0.22em]">{currentLocale}</span> : null}
|
||||
<Image
|
||||
src={localeFlags[currentLocale].src}
|
||||
alt={localeFlags[currentLocale].alt}
|
||||
width={18}
|
||||
height={18}
|
||||
className="h-[18px] w-[18px] rounded-full object-cover transition-transform duration-300 ease-out group-hover:scale-110"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-[8rem]">
|
||||
{locales.map((targetLocale) => (
|
||||
<DropdownMenuItem key={targetLocale} asChild>
|
||||
<DropdownMenuContent align="end" className={cn("w-auto min-w-0", contentFontClassName)}>
|
||||
{availableLocales.map((targetLocale) => (
|
||||
<DropdownMenuItem
|
||||
key={targetLocale}
|
||||
asChild
|
||||
className={cn(
|
||||
"cursor-pointer",
|
||||
targetLocale === "ar" ? "font-arabic" : "font-latin",
|
||||
)}
|
||||
>
|
||||
<a
|
||||
href={getLocalizedPath(targetLocale, currentPath)}
|
||||
className="flex items-center justify-between gap-4"
|
||||
className="group flex items-center justify-center"
|
||||
>
|
||||
<span>{targetLocale.toUpperCase()}</span>
|
||||
{targetLocale === currentLocale ? (
|
||||
<span className="text-xs text-muted-foreground">Active</span>
|
||||
) : null}
|
||||
<Image
|
||||
src={localeFlags[targetLocale].src}
|
||||
alt={localeFlags[targetLocale].alt}
|
||||
width={18}
|
||||
height={18}
|
||||
className="h-[18px] w-[18px] rounded-full object-cover transition-transform duration-300 ease-out group-hover:scale-110"
|
||||
/>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { HeroAtmosphere } from "@/components/layout/hero-atmosphere";
|
||||
import { MotionFade } from "@/components/motion-fade";
|
||||
import { HeroMotionBackdrop } from "@/components/layout/hero-motion-backdrop";
|
||||
|
||||
type PageHeroProps = {
|
||||
title: string;
|
||||
@@ -8,22 +7,22 @@ type PageHeroProps = {
|
||||
|
||||
export function PageHero({ title, description }: PageHeroProps) {
|
||||
return (
|
||||
<section className="relative isolate flex min-h-[42vh] items-end overflow-hidden">
|
||||
<HeroAtmosphere compact />
|
||||
<section className="hero-surface hero-page relative isolate flex min-h-[42vh] items-end overflow-hidden">
|
||||
<HeroMotionBackdrop compact />
|
||||
<div className="relative z-10 mx-auto w-full max-w-[72rem] px-4 pb-10 pt-24 sm:px-6 lg:px-8 lg:pb-12 lg:pt-28">
|
||||
<MotionFade className="max-w-[44rem]">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-foreground/48">
|
||||
<div className="mx-auto max-w-[42rem] text-center">
|
||||
<p className="text-sm font-medium tracking-[-0.02em] text-foreground/58">
|
||||
{title}
|
||||
</p>
|
||||
<h1 className="mt-4 text-balance text-4xl font-semibold leading-[0.95] tracking-[-0.05em] text-foreground sm:text-5xl lg:text-6xl">
|
||||
<h1 className="mt-4 text-balance text-4xl font-semibold leading-[0.9] tracking-[-0.07em] text-foreground sm:text-5xl lg:text-[4.5rem]">
|
||||
{title}
|
||||
</h1>
|
||||
{description ? (
|
||||
<p className="mt-5 max-w-[36rem] text-sm leading-6 text-foreground/66 sm:text-base">
|
||||
<p className="mx-auto mt-5 max-w-[32rem] text-sm leading-6 text-foreground/64 sm:text-base">
|
||||
{description}
|
||||
</p>
|
||||
) : null}
|
||||
</MotionFade>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -61,9 +61,11 @@ function NavLinks({
|
||||
key={item.key}
|
||||
href={getLocalizedPath(locale, itemPath)}
|
||||
className={cn(
|
||||
"relative z-10 rounded-[var(--radius-pill)] text-sm font-medium transition-colors",
|
||||
mobile ? "px-4 py-3" : "px-3.5 py-2 xl:px-4",
|
||||
isActive ? "text-primary-foreground" : "text-muted-foreground hover:text-foreground",
|
||||
"relative z-10 rounded-[var(--radius-pill)] transition-colors",
|
||||
mobile ? "px-4 py-3 text-sm font-medium" : "px-3.5 py-2 text-sm font-medium xl:px-4",
|
||||
isActive
|
||||
? "text-primary-foreground"
|
||||
: "text-[hsl(var(--hero-ink)/0.62)] hover:text-[hsl(var(--hero-ink))] dark:text-muted-foreground dark:hover:text-foreground",
|
||||
)}
|
||||
onClick={onNavigate}
|
||||
>
|
||||
@@ -92,21 +94,23 @@ export function SiteHeader({
|
||||
const pathname = usePathname();
|
||||
const locale = useLocale();
|
||||
const t = useTranslations("navigation");
|
||||
const isArabic = locale === "ar";
|
||||
|
||||
return (
|
||||
<header className="fixed inset-x-0 top-3 z-40 bg-transparent">
|
||||
<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="flex items-center">
|
||||
<div className="col-start-1 flex items-center justify-self-start">
|
||||
<Link
|
||||
href={getLocalizedPath(locale)}
|
||||
className="inline-flex items-center py-1 sm:py-1.5 lg:py-2"
|
||||
className="inline-flex items-center rounded-[var(--radius-pill)] px-1 py-1 sm:py-1.5 lg:py-2"
|
||||
>
|
||||
<SiteLogo
|
||||
lightLogoUrl={lightLogoUrl}
|
||||
darkLogoUrl={darkLogoUrl}
|
||||
alt="mohfarawati"
|
||||
priority
|
||||
className="h-7 opacity-88 sm:h-8 lg:h-9"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -116,7 +120,10 @@ export function SiteHeader({
|
||||
initial={{ opacity: 0, y: -8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.28, ease: "easeOut" }}
|
||||
className="pointer-events-auto inline-flex min-w-[27rem] max-w-[42rem] items-center justify-center gap-1 rounded-[var(--radius-pill)] border border-white/18 bg-white/10 p-1.5 shadow-[0_18px_44px_-28px_rgba(15,23,42,0.24)] backdrop-blur-[24px] dark:border-white/10 dark:bg-white/5 dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)]"
|
||||
className={cn(
|
||||
"pointer-events-auto inline-flex items-center gap-1 rounded-[var(--radius-pill)] border border-black/6 bg-white/24 p-1.5 shadow-[0_14px_34px_-24px_rgba(15,23,42,0.18)] backdrop-blur-[18px] dark:border-white/10 dark:bg-white/4 dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)]",
|
||||
isArabic ? "w-auto max-w-none" : "min-w-[27rem] max-w-[42rem]",
|
||||
)}
|
||||
>
|
||||
<NavLinks />
|
||||
</motion.nav>
|
||||
@@ -127,15 +134,15 @@ export function SiteHeader({
|
||||
initial={{ opacity: 0, y: -8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.28, ease: "easeOut", delay: 0.03 }}
|
||||
className="inline-flex items-center gap-1 rounded-[var(--radius-pill)] border border-white/18 bg-white/10 p-1 shadow-[0_18px_44px_-28px_rgba(15,23,42,0.24)] backdrop-blur-[24px] dark:border-white/10 dark:bg-white/5 dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)]"
|
||||
className="inline-flex items-center gap-1 rounded-[var(--radius-pill)] border border-black/6 bg-white/24 p-1 shadow-[0_14px_34px_-24px_rgba(15,23,42,0.18)] backdrop-blur-[18px] dark:border-white/10 dark:bg-white/4 dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)]"
|
||||
>
|
||||
<LocaleToggle
|
||||
locale={locale}
|
||||
showLabel
|
||||
className="h-9 rounded-[var(--radius-pill)] px-3 text-foreground hover:bg-white/34 dark:hover:bg-white/10"
|
||||
className="h-9 rounded-[var(--radius-pill)] px-3 text-[hsl(var(--hero-ink)/0.76)] hover:bg-white/36 hover:text-[hsl(var(--hero-ink))] dark:text-foreground dark:hover:bg-white/10"
|
||||
/>
|
||||
{isAdmin ? (
|
||||
<Button asChild variant="ghost" size="icon" className="h-9 w-9 rounded-[var(--radius-pill)] border border-transparent text-foreground hover:border-white/12 hover:bg-white/34 dark:hover:border-white/10 dark:hover:bg-white/10">
|
||||
<Button asChild variant="ghost" size="icon" className="h-9 w-9 rounded-[var(--radius-pill)] border border-transparent text-[hsl(var(--hero-ink)/0.76)] hover:border-black/5 hover:bg-white/36 hover:text-[hsl(var(--hero-ink))] dark:text-foreground dark:hover:border-white/10 dark:hover:bg-white/10">
|
||||
<Link href="/root" aria-label={t("root")}>
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
</Link>
|
||||
@@ -144,7 +151,7 @@ export function SiteHeader({
|
||||
<ThemeToggle
|
||||
ariaLabel={t("themeToggle")}
|
||||
variant="ghost"
|
||||
className="h-9 w-9 rounded-[var(--radius-pill)] border border-transparent text-foreground hover:border-white/12 hover:bg-white/34 dark:hover:border-white/10 dark:hover:bg-white/10"
|
||||
className="h-9 w-9 rounded-[var(--radius-pill)] border border-transparent text-[hsl(var(--hero-ink)/0.76)] hover:border-black/5 hover:bg-white/36 hover:text-[hsl(var(--hero-ink))] dark:text-foreground dark:hover:border-white/10 dark:hover:bg-white/10"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
@@ -154,7 +161,7 @@ export function SiteHeader({
|
||||
onClick={() => setIsOpen((open) => !open)}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-auto h-11 w-11 rounded-[var(--radius-pill)] border border-white/18 bg-white/12 text-foreground shadow-[0_18px_44px_-28px_rgba(15,23,42,0.24)] backdrop-blur-[24px] hover:bg-white/18 dark:border-white/10 dark:bg-white/5 dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)] dark:hover:bg-white/10 lg:hidden"
|
||||
className="col-start-3 ml-auto h-11 w-11 justify-self-end rounded-[var(--radius-pill)] border border-black/6 bg-white/24 text-[hsl(var(--hero-ink))] shadow-[0_14px_34px_-24px_rgba(15,23,42,0.18)] backdrop-blur-[18px] hover:bg-white/34 dark:border-white/10 dark:bg-white/4 dark:text-foreground dark:shadow-[0_20px_48px_-28px_rgba(0,0,0,0.58)] dark:hover:bg-white/10 lg:hidden"
|
||||
aria-label={isOpen ? t("closeMenu") : t("openMenu")}
|
||||
>
|
||||
<motion.span
|
||||
@@ -183,7 +190,7 @@ export function SiteHeader({
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.98 }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
className="overflow-hidden rounded-surface border border-white/24 bg-[linear-gradient(180deg,rgba(255,255,255,0.24),rgba(255,255,255,0.1))] shadow-[inset_0_1px_0_rgba(255,255,255,0.46),inset_0_-1px_0_rgba(255,255,255,0.08),0_18px_44px_-28px_rgba(15,23,42,0.32)] backdrop-blur-[28px] dark:border-white/12 dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.09),rgba(255,255,255,0.03))] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.12),inset_0_-1px_0_rgba(255,255,255,0.03),0_20px_48px_-28px_rgba(0,0,0,0.72)]"
|
||||
className="overflow-hidden rounded-surface border border-black/6 bg-[linear-gradient(180deg,rgba(255,255,255,0.46),rgba(255,255,255,0.24))] shadow-[inset_0_1px_0_rgba(255,255,255,0.54),0_16px_34px_-24px_rgba(15,23,42,0.24)] backdrop-blur-[20px] dark:border-white/12 dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.09),rgba(255,255,255,0.03))] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.12),inset_0_-1px_0_rgba(255,255,255,0.03),0_20px_48px_-28px_rgba(0,0,0,0.72)]"
|
||||
>
|
||||
<motion.nav
|
||||
initial="closed"
|
||||
@@ -223,8 +230,8 @@ export function SiteHeader({
|
||||
className={cn(
|
||||
"flex items-center justify-between rounded-nested px-4 py-3 text-sm font-medium transition-colors",
|
||||
isActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-foreground hover:bg-accent hover:text-accent-foreground",
|
||||
? "bg-white/72 text-[hsl(var(--hero-ink))] shadow-[0_10px_20px_-18px_rgba(15,23,42,0.24)] dark:bg-white/10 dark:text-foreground"
|
||||
: "text-[hsl(var(--hero-ink)/0.78)] hover:bg-white/40 hover:text-[hsl(var(--hero-ink))] dark:text-foreground dark:hover:bg-white/6",
|
||||
)}
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
@@ -243,17 +250,17 @@ export function SiteHeader({
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 8 }}
|
||||
transition={{ duration: 0.18, ease: "easeOut", delay: 0.04 }}
|
||||
className="border-t border-white/18 bg-[linear-gradient(180deg,rgba(255,255,255,0.14),rgba(255,255,255,0.06))] p-2 dark:border-white/8 dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02))]"
|
||||
className="border-t border-black/6 bg-[linear-gradient(180deg,rgba(255,255,255,0.2),rgba(255,255,255,0.08))] p-2 dark:border-white/8 dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02))]"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<LocaleToggle
|
||||
locale={locale}
|
||||
showLabel
|
||||
className="h-10 rounded-nested px-3 text-foreground hover:bg-white/34 dark:hover:bg-white/10"
|
||||
className="h-10 rounded-nested px-3 text-[hsl(var(--hero-ink)/0.8)] hover:bg-white/36 hover:text-[hsl(var(--hero-ink))] dark:text-foreground dark:hover:bg-white/10"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
{isAdmin ? (
|
||||
<Button asChild variant="ghost" size="icon" className="h-10 w-10 rounded-nested border border-white/18 bg-white/18 text-foreground hover:bg-white/34 dark:border-white/10 dark:bg-white/6 dark:hover:bg-white/10">
|
||||
<Button asChild variant="ghost" size="icon" className="h-10 w-10 rounded-nested border border-black/6 bg-white/32 text-[hsl(var(--hero-ink)/0.8)] hover:bg-white/42 hover:text-[hsl(var(--hero-ink))] dark:border-white/10 dark:bg-white/6 dark:text-foreground dark:hover:bg-white/10">
|
||||
<Link href="/root" aria-label={t("root")} onClick={() => setIsOpen(false)}>
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
</Link>
|
||||
@@ -262,7 +269,7 @@ export function SiteHeader({
|
||||
<ThemeToggle
|
||||
ariaLabel={t("themeToggle")}
|
||||
variant="ghost"
|
||||
className="h-10 w-10 rounded-nested border border-white/18 bg-white/18 text-foreground hover:bg-white/34 dark:border-white/10 dark:bg-white/6 dark:hover:bg-white/10"
|
||||
className="h-10 w-10 rounded-nested border border-black/6 bg-white/32 text-[hsl(var(--hero-ink)/0.8)] hover:bg-white/42 hover:text-[hsl(var(--hero-ink))] dark:border-white/10 dark:bg-white/6 dark:text-foreground dark:hover:bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type SiteLogoProps = {
|
||||
lightLogoUrl?: string | null;
|
||||
darkLogoUrl?: string | null;
|
||||
alt: string;
|
||||
priority?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function SiteLogo({
|
||||
@@ -16,6 +20,7 @@ export function SiteLogo({
|
||||
darkLogoUrl,
|
||||
alt,
|
||||
priority = false,
|
||||
className,
|
||||
}: SiteLogoProps) {
|
||||
const { theme, resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
@@ -30,16 +35,23 @@ export function SiteLogo({
|
||||
const src = isDark ? darkLogoUrl || lightLogoUrl || fallbackSrc : lightLogoUrl || darkLogoUrl || fallbackSrc;
|
||||
|
||||
return (
|
||||
<span className="block">
|
||||
<motion.span
|
||||
className="block"
|
||||
whileHover={{ scale: 1.04, y: -1 }}
|
||||
transition={{ duration: 0.28, ease: "easeOut" }}
|
||||
>
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={360}
|
||||
height={92}
|
||||
sizes="(min-width: 1280px) 360px, (min-width: 1024px) 320px, (min-width: 640px) 280px, 240px"
|
||||
className="h-9 w-auto object-contain object-left sm:h-10 lg:h-11 xl:h-12"
|
||||
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",
|
||||
className,
|
||||
)}
|
||||
priority={priority}
|
||||
/>
|
||||
</span>
|
||||
</motion.span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ export function ThemeToggle({
|
||||
variant={variant}
|
||||
size={label ? "sm" : "icon"}
|
||||
aria-label={ariaLabel}
|
||||
className={cn(label ? "justify-start" : undefined, className)}
|
||||
className={cn("group cursor-pointer", label ? "justify-start" : undefined, className)}
|
||||
>
|
||||
<Moon className={cn("h-4 w-4", iconClassName)} />
|
||||
<Moon className={cn("h-4 w-4 transition-transform duration-700 ease-out group-hover:scale-110 group-hover:rotate-[360deg]", iconClassName)} />
|
||||
{label ? <span>{label}</span> : null}
|
||||
</Button>
|
||||
);
|
||||
@@ -54,9 +54,13 @@ export function ThemeToggle({
|
||||
size={label ? "sm" : "icon"}
|
||||
onClick={() => setTheme(isDark ? "light" : "dark")}
|
||||
aria-label={ariaLabel}
|
||||
className={cn(label ? "justify-start" : undefined, className)}
|
||||
className={cn("group cursor-pointer", label ? "justify-start" : undefined, className)}
|
||||
>
|
||||
{isDark ? <Sun className={cn("h-4 w-4", iconClassName)} /> : <Moon className={cn("h-4 w-4", iconClassName)} />}
|
||||
{isDark ? (
|
||||
<Sun className={cn("h-4 w-4 transition-transform duration-700 ease-out group-hover:scale-110 group-hover:rotate-[360deg]", iconClassName)} />
|
||||
) : (
|
||||
<Moon className={cn("h-4 w-4 transition-transform duration-700 ease-out group-hover:scale-110 group-hover:rotate-[360deg]", iconClassName)} />
|
||||
)}
|
||||
{label ? <span>{label}</span> : null}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -60,7 +60,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[12rem] overflow-hidden rounded-surface border border-border/80 bg-popover p-1 text-popover-foreground shadow-panel",
|
||||
"z-50 min-w-[12rem] overflow-hidden rounded-surface border border-border/80 bg-popover p-1 text-popover-foreground shadow-panel data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user