Add responsive branded header and sound controls
CI / quality (push) Waiting to run

This commit is contained in:
MOH
2026-03-09 06:54:47 +01:00
parent a65037e3b3
commit 9d76eabb39
16 changed files with 616 additions and 136 deletions
@@ -3,6 +3,7 @@
import { useTranslations } from "next-intl";
import { LocaleToggle } from "@/components/layout/locale-toggle";
import { SoundToggle } from "@/components/sound-toggle";
import { ThemeToggle } from "@/components/theme-toggle";
import { cn } from "@/lib/utils";
@@ -21,6 +22,12 @@ export function FloatingPreferences({
<div className={cn("fixed inset-x-0 top-0 z-50 px-4 pt-4 sm:px-6", className)}>
<div className="mx-auto flex w-full justify-center">
<div className="inline-flex items-center justify-center gap-1 rounded-pill border border-border/70 bg-background/80 p-1 shadow-panel backdrop-blur-chrome">
<SoundToggle
ariaLabel={t("soundMute")}
mutedAriaLabel={t("soundUnmute")}
variant="ghost"
className="h-9 w-9 rounded-pill border border-transparent bg-transparent text-foreground/80 hover:bg-accent hover:text-foreground"
/>
<ThemeToggle
ariaLabel={t("themeToggle")}
variant="ghost"
+95 -10
View File
@@ -10,6 +10,7 @@ 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 { SoundToggle } from "@/components/sound-toggle";
import { ThemeToggle } from "@/components/theme-toggle";
import { Button } from "@/components/ui/button";
import { getLocalizedPath, stripLocalePrefix } from "@/lib/locale";
@@ -23,6 +24,41 @@ const navItems = [
{ 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 = {
isAdmin?: boolean;
lightLogoUrl?: string | null;
@@ -109,15 +145,52 @@ export function SiteHeader({
<div className="col-start-1 flex items-center justify-self-start">
<Link
href={getLocalizedPath(locale)}
className="inline-flex items-center rounded-pill px-1 py-1 sm:py-1.5 lg:py-2"
className="inline-flex items-center gap-2.5 rounded-pill px-1 py-1 sm:gap-3 lg:py-2"
>
<SiteLogo
lightLogoUrl={lightLogoUrl}
darkLogoUrl={darkLogoUrl}
alt="mohfarawati"
priority
className="h-7 opacity-88 sm:h-8 lg:h-9"
/>
<motion.span
initial={{ opacity: 0, x: -18, scale: 0.82, rotate: -14, filter: "blur(10px)" }}
animate={{ opacity: 1, x: 0, scale: 1, rotate: 0, filter: "blur(0px)" }}
transition={{ type: "spring", stiffness: 240, damping: 20, mass: 0.9 }}
className="relative inline-flex items-center justify-center [transform-style:preserve-3d]"
>
<SiteLogo
lightLogoUrl={lightLogoUrl}
darkLogoUrl={darkLogoUrl}
alt="mohfarawati"
priority
className="relative h-10 opacity-95"
/>
</motion.span>
<motion.span
initial="hidden"
animate="visible"
variants={brandShellVariants}
className="flex flex-col justify-center"
>
<span className="flex flex-wrap items-center text-[1.28rem] font-black leading-[1.2]">
{brandTitleCharacters.map(({ character, tone }, index) => (
<motion.span
key={`${character}-${index}`}
variants={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>
))}
</span>
<motion.span
initial={{ opacity: 0, y: 20, clipPath: "inset(0 0 100% 0)", filter: "blur(10px)" }}
animate={{ opacity: 1, y: 0, clipPath: "inset(0 0 0% 0)", filter: "blur(0px)" }}
transition={{ duration: 0.7, delay: 0.34, ease: [0.22, 1, 0.36, 1] }}
className="-mt-0.5 text-[0.65rem] font-light leading-[1.2] tracking-[0.008em] text-foreground/78"
>
Webdesigner Bremen | Freelancer
</motion.span>
</motion.span>
</Link>
</div>
@@ -146,6 +219,12 @@ export function SiteHeader({
locale={locale}
className={desktopControlButtonClassName}
/>
<SoundToggle
ariaLabel={t("soundMute")}
mutedAriaLabel={t("soundUnmute")}
variant="ghost"
className={desktopControlButtonClassName}
/>
<ThemeToggle
ariaLabel={t("themeToggle")}
variant="ghost"
@@ -154,7 +233,7 @@ export function SiteHeader({
{isAdmin ? (
<Button asChild variant="ghost" size="icon" className={desktopControlButtonClassName}>
<Link href="/root" aria-label={t("root")}>
<LayoutDashboard className="h-4 w-4" />
<LayoutDashboard className="h-4 w-4 transition-transform duration-300 ease-out hover:scale-110" />
</Link>
</Button>
) : null}
@@ -263,6 +342,12 @@ export function SiteHeader({
className={mobileControlButtonClassName}
/>
<div className="flex items-center gap-2">
<SoundToggle
ariaLabel={t("soundMute")}
mutedAriaLabel={t("soundUnmute")}
variant="ghost"
className={mobileControlButtonClassName}
/>
<ThemeToggle
ariaLabel={t("themeToggle")}
variant="ghost"
@@ -271,7 +356,7 @@ export function SiteHeader({
{isAdmin ? (
<Button asChild variant="ghost" size="icon" className={mobileControlButtonClassName}>
<Link href="/root" aria-label={t("root")} onClick={() => setIsOpen(false)}>
<LayoutDashboard className="h-4 w-4" />
<LayoutDashboard className="h-4 w-4 transition-transform duration-300 ease-out hover:scale-110" />
</Link>
</Button>
) : null}
+6 -3
View File
@@ -36,9 +36,12 @@ export function SiteLogo({
return (
<motion.span
className="block"
whileHover={{ scale: 1.04, y: -1 }}
transition={{ duration: 0.28, ease: "easeOut" }}
className="block [transform-style:preserve-3d]"
initial={{ opacity: 0, x: -42, scale: 0.88, rotate: -360 }}
animate={{ opacity: 1, x: 0, scale: 1, rotate: 0 }}
whileHover={{ scale: 1.05, y: -2, rotate: -2, filter: "drop-shadow(0 16px 28px rgba(15,23,42,0.14))" }}
whileTap={{ scale: 0.9, rotate: 6, y: 1, filter: "drop-shadow(0 8px 18px rgba(15,23,42,0.18))" }}
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] } }}
>
<Image
src={src}