feat(ui): Kopfzeile, Fusszeile und Details mit mehr Leben
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 4m11s
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 4m11s
- Kopfzeile: aktive Seite mit Honig-Unterstrich (auch im Handy-Menue), einlaufende Unterstriche beim Hover, beim Scrollen Glas und Schatten; weicht beim Runterscrollen aus und ist beim Hochscrollen sofort zurueck (steht still, solange Warenkorb oder Menue offen sind) - Fusszeile mit Wabenraster und einlaufenden Link-Unterstrichen - Mengensteller: gezeichnete Plus/Minus, taktiles Druecken, tickende Zahl - Newsletter als eigene Flaeche mit Wabenraster; Erfolg zeichnet ein Haekchen in eine Wabe, Fehler werden als Alert angesagt - 404: Wabe mit einer fehlenden Zelle als Bild fuer die fehlende Seite Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
4ee5bafe20
commit
064ca3ff6d
5 files changed
+188
-25
No files matched your search
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState, useEffect, useRef, type RefObject } from "react";
|
||||
import { AnimatePresence, motion, useAnimationControls } from "motion/react";
|
||||
import { useCartStore, cartItemCount, cartTotalCents } from "@/store/cart-store";
|
||||
@@ -14,6 +15,7 @@ import { QuantityStepper } from "@/components/quantity-stepper";
|
||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||
import { useI18n } from "@/i18n/i18n-provider";
|
||||
import { CartFlight, onCartLanded } from "@/components/cart-flight";
|
||||
import { HexMark } from "@/components/honeycomb";
|
||||
import type { Dictionary } from "@/i18n/dictionary-type";
|
||||
|
||||
const FOCUSABLE_SELECTOR =
|
||||
@@ -271,9 +273,52 @@ function CartIconButton() {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kopfzeile reagiert aufs Scrollen: ab etwas Abstand zum Seitenanfang
|
||||
* bekommt sie Schatten und staerkeres Glas; beim Weiterlesen nach unten
|
||||
* weicht sie aus, beim kleinsten Hochscrollen ist sie sofort wieder da.
|
||||
* Waehrend Warenkorb oder Menue offen sind, bleibt sie stehen.
|
||||
*/
|
||||
function useHeaderScrollState(locked: boolean) {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [hidden, setHidden] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let lastY = window.scrollY;
|
||||
let frame = 0;
|
||||
const onScroll = () => {
|
||||
cancelAnimationFrame(frame);
|
||||
frame = requestAnimationFrame(() => {
|
||||
const y = window.scrollY;
|
||||
setScrolled(y > 12);
|
||||
// Kleine Zitterbewegungen (Trackpad) ignorieren.
|
||||
if (Math.abs(y - lastY) > 6) {
|
||||
setHidden(y > lastY && y > 240);
|
||||
lastY = y;
|
||||
}
|
||||
});
|
||||
};
|
||||
onScroll();
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { scrolled, hidden: hidden && !locked };
|
||||
}
|
||||
|
||||
function isActivePath(pathname: string, href: string): boolean {
|
||||
return href === "/" ? pathname === "/" : pathname === href || pathname.startsWith(`${href}/`);
|
||||
}
|
||||
|
||||
export function Header({ companyName }: { companyName: string }) {
|
||||
const { t } = useI18n();
|
||||
const pathname = usePathname();
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const cartOpen = useCartStore((s) => s.isOpen);
|
||||
const { scrolled, hidden } = useHeaderScrollState(mobileOpen || cartOpen);
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const mobileMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -287,7 +332,15 @@ export function Header({ companyName }: { companyName: string }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="sticky top-0 z-40 bg-background/95 backdrop-blur border-b border-ink/10">
|
||||
<header
|
||||
className={`sticky top-0 z-40 border-b transition-[transform,background-color,box-shadow,border-color] duration-500 ease-[var(--ease-arrive)] motion-reduce:transition-none ${
|
||||
hidden ? "-translate-y-full" : "translate-y-0"
|
||||
} ${
|
||||
scrolled
|
||||
? "bg-background/80 backdrop-blur-md border-transparent shadow-[0_10px_30px_-20px_rgba(36,53,43,0.45)]"
|
||||
: "bg-background/95 backdrop-blur border-ink/10"
|
||||
}`}
|
||||
>
|
||||
{/*
|
||||
Eigener, breiterer Wrapper statt der gemeinsamen <Container> (max-w-6xl):
|
||||
Logo + 7-Punkte-Navigation + CTA-Button + Icons brauchen mehr Raum als die
|
||||
@@ -298,15 +351,21 @@ export function Header({ companyName }: { companyName: string }) {
|
||||
<Logo t={t} companyName={companyName} />
|
||||
|
||||
<nav aria-label="Hauptnavigation" className="hidden xl:flex items-center gap-3 2xl:gap-6">
|
||||
{mainNav.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="text-sm text-ink/80 hover:text-forest transition-colors whitespace-nowrap"
|
||||
>
|
||||
{t.nav[item.key]}
|
||||
</Link>
|
||||
))}
|
||||
{mainNav.map((item) => {
|
||||
const active = isActivePath(pathname, item.href);
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={`link-draw pb-1 text-sm whitespace-nowrap transition-colors ${
|
||||
active ? "text-forest [background-image:linear-gradient(var(--color-honey),var(--color-honey))] [background-size:100%_2px]" : "text-ink/80 hover:text-forest"
|
||||
}`}
|
||||
>
|
||||
{t.nav[item.key]}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -396,8 +455,12 @@ export function Header({ companyName }: { companyName: string }) {
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="text-3xl font-serif"
|
||||
aria-current={isActivePath(pathname, item.href) ? "page" : undefined}
|
||||
className={`flex items-center gap-3 text-3xl font-serif ${
|
||||
isActivePath(pathname, item.href) ? "text-honey-light" : ""
|
||||
}`}
|
||||
>
|
||||
{isActivePath(pathname, item.href) ? <HexMark className="h-3 w-3" /> : null}
|
||||
{t.nav[item.key]}
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
Reference in new issue
Block a user