Refactor site header, heroes, and design system

This commit is contained in:
MOH
2026-03-08 00:29:51 +01:00
parent a18370d003
commit 0c2b67e378
56 changed files with 2714 additions and 1318 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ export function AppSidebar({
<Link
href={item.href}
className={cn(
"flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors",
"flex items-center gap-2 rounded-nested px-3 py-2 text-sm transition-colors",
item.active
? "bg-sidebar-primary text-sidebar-primary-foreground"
: "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+70
View File
@@ -0,0 +1,70 @@
"use client";
import { motion } from "framer-motion";
type HeroAtmosphereProps = {
compact?: boolean;
};
export function HeroAtmosphere({ compact = false }: HeroAtmosphereProps) {
return (
<div className="pointer-events-none absolute inset-0 overflow-hidden">
<motion.div
animate={{
x: [0, 90, -48, 0],
y: [0, -70, 32, 0],
scale: [1, 1.12, 0.96, 1],
}}
transition={{
duration: 20,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
className="absolute left-[-10%] top-[4%] h-[24rem] w-[24rem] rounded-full bg-brand-primary/22 blur-3xl sm:h-[34rem] sm:w-[34rem] lg:h-[42rem] lg:w-[42rem]"
/>
<motion.div
animate={{
x: [0, -110, 40, 0],
y: [0, 48, -26, 0],
scale: [1, 0.92, 1.08, 1],
}}
transition={{
duration: 24,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
className="absolute right-[-14%] top-[2%] h-[22rem] w-[22rem] rounded-full bg-brand-secondary/18 blur-3xl sm:h-[30rem] sm:w-[30rem] lg:h-[38rem] lg:w-[38rem]"
/>
<motion.div
animate={{
x: [0, 42, -24, 0],
y: [0, -28, 30, 0],
opacity: [0.2, 0.34, 0.18, 0.2],
}}
transition={{
duration: 18,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
className="absolute bottom-[-18%] left-[18%] h-[18rem] w-[18rem] rounded-full bg-brand-primary/14 blur-3xl sm:h-[24rem] sm:w-[24rem] lg:h-[30rem] lg:w-[30rem]"
/>
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.58),rgba(255,255,255,0.32)_32%,rgba(255,255,255,0.08)_68%,rgba(255,255,255,0.02)),radial-gradient(circle_at_18%_24%,rgba(221,65,36,0.16),transparent_30%),radial-gradient(circle_at_78%_20%,rgba(235,143,117,0.18),transparent_34%),radial-gradient(circle_at_50%_100%,rgba(174,196,255,0.18),transparent_38%)] dark:bg-[linear-gradient(180deg,rgba(10,14,22,0.3),rgba(10,14,22,0.42)_34%,rgba(10,14,22,0.7)_100%),radial-gradient(circle_at_18%_24%,rgba(221,65,36,0.2),transparent_30%),radial-gradient(circle_at_78%_20%,rgba(235,143,117,0.12),transparent_34%),radial-gradient(circle_at_50%_100%,rgba(91,117,214,0.12),transparent_38%)]" />
<motion.div
animate={{
x: [0, compact ? 18 : 26, compact ? -12 : -18, 0],
y: [0, compact ? -14 : -22, compact ? 10 : 16, 0],
}}
transition={{
duration: compact ? 8 : 10,
repeat: Number.POSITIVE_INFINITY,
ease: "linear",
}}
className="hero-noise absolute inset-[-8%] opacity-55 mix-blend-soft-light dark:opacity-22"
/>
</div>
);
}
+57
View File
@@ -0,0 +1,57 @@
import { ArrowRight } from "lucide-react";
import Link from "next/link";
import { HeroAtmosphere } from "@/components/layout/hero-atmosphere";
import { MotionFade } from "@/components/motion-fade";
import { Button } from "@/components/ui/button";
import { getLocalizedPath } from "@/lib/locale";
type HomeHeroProps = {
locale: string;
kicker: string;
title: string;
description: string;
portfolioLabel: string;
contactLabel: string;
};
export function HomeHero({
locale,
kicker,
title,
description,
portfolioLabel,
contactLabel,
}: HomeHeroProps) {
return (
<section className="relative isolate flex min-h-[85vh] items-center overflow-hidden">
<HeroAtmosphere />
<div className="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>
</MotionFade>
</div>
</section>
);
}
+12 -3
View File
@@ -11,12 +11,15 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { AppLocale, getLocalizedPath, stripLocalePrefix } from "@/lib/locale";
import { cn } from "@/lib/utils";
type LocaleToggleProps = {
locale: string;
className?: string;
showLabel?: boolean;
};
export function LocaleToggle({ locale }: LocaleToggleProps) {
export function LocaleToggle({ locale, className, showLabel = false }: LocaleToggleProps) {
const pathname = usePathname();
const locales: AppLocale[] = ["de", "en", "ar"];
const currentPath = stripLocalePrefix(pathname);
@@ -27,11 +30,17 @@ export function LocaleToggle({ locale }: LocaleToggleProps) {
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="icon"
variant="ghost"
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",
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}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[8rem]">
+30
View File
@@ -0,0 +1,30 @@
import { HeroAtmosphere } from "@/components/layout/hero-atmosphere";
import { MotionFade } from "@/components/motion-fade";
type PageHeroProps = {
title: string;
description?: string;
};
export function PageHero({ title, description }: PageHeroProps) {
return (
<section className="relative isolate flex min-h-[42vh] items-end overflow-hidden">
<HeroAtmosphere compact />
<div className="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">
{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">
{title}
</h1>
{description ? (
<p className="mt-5 max-w-[36rem] text-sm leading-6 text-foreground/66 sm:text-base">
{description}
</p>
) : null}
</MotionFade>
</div>
</section>
);
}
@@ -0,0 +1,62 @@
"use client";
import { motion } from "framer-motion";
export function SiteAmbientBackdrop() {
return (
<div className="pointer-events-none fixed inset-0 -z-10 overflow-hidden">
<motion.div
animate={{
x: [0, 72, -36, 0],
y: [0, -54, 28, 0],
scale: [1, 1.1, 0.95, 1],
}}
transition={{
duration: 18,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
className="absolute left-[-10%] top-[4%] h-[30rem] w-[30rem] rounded-full bg-brand-primary/18 blur-3xl sm:h-[38rem] sm:w-[38rem] lg:h-[46rem] lg:w-[46rem]"
/>
<motion.div
animate={{
x: [0, -84, 26, 0],
y: [0, 44, -22, 0],
scale: [1, 0.9, 1.08, 1],
}}
transition={{
duration: 22,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
className="absolute right-[-12%] top-[8%] h-[26rem] w-[26rem] rounded-full bg-brand-secondary/16 blur-3xl sm:h-[34rem] sm:w-[34rem] lg:h-[42rem] lg:w-[42rem]"
/>
<motion.div
animate={{
x: [0, 34, -20, 0],
y: [0, -24, 34, 0],
opacity: [0.22, 0.38, 0.16, 0.22],
}}
transition={{
duration: 16,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
}}
className="absolute bottom-[-12%] left-[22%] h-[20rem] w-[20rem] rounded-full bg-brand-primary/14 blur-3xl sm:h-[28rem] sm:w-[28rem] lg:h-[34rem] lg:w-[34rem]"
/>
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.64),rgba(255,255,255,0.42)_24%,rgba(255,255,255,0.14)_52%,rgba(255,255,255,0.02)_100%),radial-gradient(circle_at_16%_18%,rgba(221,65,36,0.12),transparent_30%),radial-gradient(circle_at_78%_14%,rgba(235,143,117,0.16),transparent_34%),radial-gradient(circle_at_50%_100%,rgba(160,182,255,0.16),transparent_40%)] dark:bg-[linear-gradient(180deg,rgba(11,15,24,0.24),rgba(11,15,24,0.36)_26%,rgba(11,15,24,0.72)_100%),radial-gradient(circle_at_16%_18%,rgba(221,65,36,0.16),transparent_30%),radial-gradient(circle_at_78%_14%,rgba(235,143,117,0.1),transparent_34%),radial-gradient(circle_at_50%_100%,rgba(109,130,214,0.12),transparent_40%)]" />
<motion.div
animate={{
x: [0, 20, -14, 0],
y: [0, -16, 12, 0],
}}
transition={{
duration: 9,
repeat: Number.POSITIVE_INFINITY,
ease: "linear",
}}
className="hero-noise absolute inset-[-6%] opacity-48 mix-blend-soft-light dark:opacity-18"
/>
</div>
);
}
+209 -64
View File
@@ -1,16 +1,19 @@
"use client";
import { Menu, X } from "lucide-react";
import Image from "next/image";
import { AnimatePresence, motion } from "framer-motion";
import { LayoutDashboard, Menu, X } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useLocale, useTranslations } from "next-intl";
import { useState } from "react";
import { Container } from "@/components/layout/container";
import { LocaleToggle } from "@/components/layout/locale-toggle";
import { SiteLogo } from "@/components/layout/site-logo";
import { ThemeToggle } from "@/components/theme-toggle";
import { Button } from "@/components/ui/button";
import { getLocalizedPath } from "@/lib/locale";
import { getLocalizedPath, stripLocalePrefix } from "@/lib/locale";
import { cn } from "@/lib/utils";
const navItems = [
{ key: "home", path: "" },
@@ -22,111 +25,253 @@ const navItems = [
type SiteHeaderProps = {
isAdmin?: boolean;
lightLogoUrl?: string | null;
darkLogoUrl?: string | null;
};
function isNavItemActive(currentPath: string, itemPath: string) {
if (itemPath === "/") {
return currentPath === "/";
}
return currentPath === itemPath || currentPath.startsWith(`${itemPath}/`);
}
function NavLinks({
isAdmin,
onNavigate,
mobile = false,
}: {
isAdmin: boolean;
onNavigate?: () => void;
mobile?: boolean;
}) {
const locale = useLocale();
const pathname = usePathname();
const t = useTranslations("navigation");
const currentPath = stripLocalePrefix(pathname);
return (
<>
{navItems.map((item) => (
<Link
key={item.key}
href={getLocalizedPath(locale, item.path || "/")}
className="text-sm text-muted-foreground hover:text-foreground"
onClick={onNavigate}
>
{t(item.key)}
</Link>
(() => {
const itemPath = item.path || "/";
const isActive = isNavItemActive(currentPath, itemPath);
return (
<Link
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",
)}
onClick={onNavigate}
>
{isActive ? (
<motion.span
layoutId="site-header-active-tab"
className="absolute inset-0 -z-10 rounded-[var(--radius-pill)] bg-primary shadow-[0_14px_34px_-22px_hsl(var(--brand-primary)/0.65)]"
transition={{ type: "spring", stiffness: 380, damping: 30 }}
/>
) : null}
{t(item.key)}
</Link>
);
})()
))}
{isAdmin ? (
<a
href="/root"
className="text-sm text-muted-foreground hover:text-foreground"
onClick={onNavigate}
>
{t("root")}
</a>
) : null}
</>
);
}
export function SiteHeader({ isAdmin = false }: SiteHeaderProps) {
export function SiteHeader({
isAdmin = false,
lightLogoUrl,
darkLogoUrl,
}: SiteHeaderProps) {
const [isOpen, setIsOpen] = useState(false);
const pathname = usePathname();
const locale = useLocale();
const t = useTranslations("navigation");
return (
<header className="sticky top-0 z-40 border-b border-border/80 bg-background/88 backdrop-blur-chrome">
<Container>
<div className="flex min-h-header items-center justify-between gap-4">
<div className="flex items-center gap-4">
<header className="sticky 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">
<Link
href={getLocalizedPath(locale)}
className="inline-flex items-center rounded-md border border-input bg-background px-3 py-2 shadow-sm"
className="inline-flex items-center py-1 sm:py-1.5 lg:py-2"
>
<Image
src="/logos/light-primary.svg"
<SiteLogo
lightLogoUrl={lightLogoUrl}
darkLogoUrl={darkLogoUrl}
alt="mohfarawati"
width={140}
height={24}
className="block h-6 w-auto dark:hidden"
priority
/>
<Image
src="/logos/dark-primary.svg"
alt="mohfarawati"
width={140}
height={24}
className="hidden h-6 w-auto dark:block"
priority
/>
</Link>
<nav className="hidden items-center gap-5 md:flex">
<NavLinks isAdmin={isAdmin} />
</nav>
</div>
<div className="hidden items-center gap-2 md:flex">
<ThemeToggle ariaLabel={t("themeToggle")} />
<LocaleToggle locale={locale} />
<div className="pointer-events-none absolute left-1/2 top-1/2 hidden -translate-x-1/2 -translate-y-1/2 lg:flex">
<motion.nav
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/24 bg-[linear-gradient(180deg,rgba(255,255,255,0.22),rgba(255,255,255,0.1))] p-1.5 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)]"
>
<NavLinks />
</motion.nav>
</div>
<div className="hidden items-center justify-end lg:flex">
<motion.div
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/24 bg-[linear-gradient(180deg,rgba(255,255,255,0.22),rgba(255,255,255,0.1))] p-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)]"
>
<LocaleToggle
locale={locale}
showLabel
className="h-9 rounded-[var(--radius-pill)] px-3 text-foreground hover:bg-white/34 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">
<Link href="/root" aria-label={t("root")}>
<LayoutDashboard className="h-4 w-4" />
</Link>
</Button>
) : null}
<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"
/>
</motion.div>
</div>
<Button
type="button"
onClick={() => setIsOpen((open) => !open)}
variant="outline"
variant="ghost"
size="icon"
className="md:hidden"
className="ml-auto h-11 w-11 rounded-[var(--radius-pill)] border border-white/24 bg-[linear-gradient(180deg,rgba(255,255,255,0.24),rgba(255,255,255,0.11))] text-foreground 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] hover:bg-[linear-gradient(180deg,rgba(255,255,255,0.3),rgba(255,255,255,0.14))] 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)] dark:hover:bg-[linear-gradient(180deg,rgba(255,255,255,0.12),rgba(255,255,255,0.05))] lg:hidden"
aria-label={isOpen ? t("closeMenu") : t("openMenu")}
>
{isOpen ? <X className="h-4 w-4" /> : <Menu className="h-4 w-4" />}
<motion.span
animate={{ rotate: isOpen ? 180 : 0, scale: isOpen ? 0.96 : 1 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="flex items-center justify-center"
>
{isOpen ? <X className="h-4 w-4" /> : <Menu className="h-4 w-4" />}
</motion.span>
</Button>
</div>
</Container>
{isOpen ? (
<div className="border-t border-border/80 bg-background md:hidden">
<Container className="py-4">
<nav className="flex flex-col gap-3">
<NavLinks isAdmin={isAdmin} onNavigate={() => setIsOpen(false)} />
</nav>
<div className="mt-4 flex items-center gap-2">
<ThemeToggle ariaLabel={t("themeToggle")} />
<LocaleToggle locale={locale} />
</div>
</Container>
</div>
) : null}
<AnimatePresence initial={false}>
{isOpen ? (
<motion.div
initial={{ opacity: 0, y: -14 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.22, ease: "easeOut" }}
className="bg-transparent lg:hidden"
>
<Container className="pb-2 pt-3">
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
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)]"
>
<motion.nav
initial="closed"
animate="open"
exit="closed"
variants={{
open: {
transition: {
staggerChildren: 0.04,
delayChildren: 0.02,
},
},
closed: {
transition: {
staggerChildren: 0.03,
staggerDirection: -1,
},
},
}}
className="flex flex-col gap-1 p-2"
>
{navItems.map((item) => {
const itemPath = item.path || "/";
const isActive = isNavItemActive(stripLocalePrefix(pathname), itemPath);
return (
<motion.div
key={item.key}
variants={{
open: { opacity: 1, y: 0 },
closed: { opacity: 0, y: -8 },
}}
transition={{ duration: 0.18, ease: "easeOut" }}
>
<Link
href={getLocalizedPath(locale, itemPath)}
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",
)}
onClick={() => setIsOpen(false)}
>
<span>{t(item.key)}</span>
<span className="text-xs uppercase tracking-[0.18em] opacity-60">
0{navItems.findIndex((navItem) => navItem.key === item.key) + 1}
</span>
</Link>
</motion.div>
);
})}
</motion.nav>
<motion.div
initial={{ opacity: 0, y: 10 }}
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))]"
>
<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"
/>
<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">
<Link href="/root" aria-label={t("root")} onClick={() => setIsOpen(false)}>
<LayoutDashboard className="h-4 w-4" />
</Link>
</Button>
) : null}
<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"
/>
</div>
</div>
</motion.div>
</motion.div>
</Container>
</motion.div>
) : null}
</AnimatePresence>
</header>
);
}
+45
View File
@@ -0,0 +1,45 @@
"use client";
import Image from "next/image";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
type SiteLogoProps = {
lightLogoUrl?: string | null;
darkLogoUrl?: string | null;
alt: string;
priority?: boolean;
};
export function SiteLogo({
lightLogoUrl,
darkLogoUrl,
alt,
priority = false,
}: SiteLogoProps) {
const { theme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const activeTheme = theme === "system" ? resolvedTheme : theme;
const isDark = mounted && activeTheme === "dark";
const fallbackSrc = isDark ? "/logos/dark-primary.svg" : "/logos/light-primary.svg";
const src = isDark ? darkLogoUrl || lightLogoUrl || fallbackSrc : lightLogoUrl || darkLogoUrl || fallbackSrc;
return (
<span className="block">
<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"
priority={priority}
/>
</span>
);
}