feat(ui): Startseite mutiger gestaltet

- Hero: deutlich groessere Headline, langsam atmendes Bild, Wabenraster,
  Honigtropfen als Scroll-Hinweis, Hauptaktion in Honig
- "Sechs Schritte" als Weg: eine Honiglinie fuellt sich beim Scrollen und
  laesst die Waben der Schritte nacheinander aufleuchten (waagerecht ab lg,
  senkrecht auf dem Handy; reduzierte Bewegung: sofort gefuellt)
- Philosophie mit Waben statt Nummern, Verantwortung als geteiltes Layout
  mit mitlaufender Ueberschrift
- Bilder mit versetztem Honigrahmen, Wabenraster auf dunklen Flaechen,
  grosse Anfuehrungszeichen bei den Kundenstimmen
- Knopf-Varianten "inverted"/"accent" fuer dunkle Flaechen ersetzen
  !-Overrides, die den Fuell-Effekt ausgehebelt haetten
- Fix: Ueberschrift der Kundenstimmen war Waldgruen auf Waldgruen

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-25 07:48:08 +02:00
1 parent 3cb639c154
commit 3de17ca070
9 files changed
+268 -73

No files matched your search

+38
View File
@@ -165,3 +165,41 @@ p {
opacity: 0.5; opacity: 0.5;
} }
} }
/* Hero: das Bild "atmet" kaum merklich (langsamer Zoom hin und zurueck). */
@keyframes wh-drift {
from {
transform: scale(1);
}
to {
transform: scale(1.07) translate(-1%, -1%);
}
}
/* Scroll-Hinweis im Hero: ein Honigtropfen loest sich und faellt. */
@keyframes wh-drip {
0%,
15% {
transform: translateY(0) scale(0.6);
opacity: 0;
}
30% {
transform: translateY(0) scale(1);
opacity: 1;
}
75% {
transform: translateY(26px) scale(0.9, 1.1);
opacity: 1;
}
100% {
transform: translateY(34px) scale(0.4);
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.motion-drift,
.motion-drip {
animation: none !important;
}
}
+2 -2
View File
@@ -49,8 +49,8 @@ export function CookieConsentBanner() {
</p> </p>
<div className="flex flex-shrink-0 flex-wrap gap-3"> <div className="flex flex-shrink-0 flex-wrap gap-3">
<Button <Button
variant="secondary" variant="inverted"
className="!border-cream !text-cream hover:!bg-cream hover:!text-forest whitespace-nowrap" className="whitespace-nowrap"
onClick={() => choose("essential")} onClick={() => choose("essential")}
> >
{t.cookieConsent.essentialOnly} {t.cookieConsent.essentialOnly}
+29 -9
View File
@@ -3,6 +3,7 @@
import { motion, useReducedMotion, type Variants } from "motion/react"; import { motion, useReducedMotion, type Variants } from "motion/react";
import { Container, LinkButton } from "@/components/ui"; import { Container, LinkButton } from "@/components/ui";
import { ContentImage } from "@/components/content-image"; import { ContentImage } from "@/components/content-image";
import { HoneycombPattern } from "@/components/honeycomb";
import { useI18n } from "@/i18n/i18n-provider"; import { useI18n } from "@/i18n/i18n-provider";
const itemContainerVariants: Variants = { const itemContainerVariants: Variants = {
@@ -45,7 +46,7 @@ export function Hero({
// weiter unten bleibt unveraendert, der laeuft unabhaengig davon. // weiter unten bleibt unveraendert, der laeuft unabhaengig davon.
<section className="relative"> <section className="relative">
{video ? ( {video ? (
<div className="relative h-[78vh] min-h-[520px] w-full overflow-hidden"> <div className="relative h-[82vh] min-h-[560px] w-full overflow-hidden">
{/* Standbild als eigene Ebene UNTER dem Video statt im {/* Standbild als eigene Ebene UNTER dem Video statt im
poster-Attribut. poster nimmt nur eine fertige URL und laedt die poster-Attribut. poster nimmt nur eine fertige URL und laedt die
hochgeladene Datei damit in voller gespeicherter Groesse (bis hochgeladene Datei damit in voller gespeicherter Groesse (bis
@@ -55,7 +56,7 @@ export function Hero({
Bildschirm, WebP statt JPEG, und mit "priority" als erstes Bildschirm, WebP statt JPEG, und mit "priority" als erstes
angefordert. Das Video liegt darueber und ist transparent, bis es angefordert. Das Video liegt darueber und ist transparent, bis es
eigene Bilddaten hat — der Uebergang ist deshalb nicht sichtbar. */} eigene Bilddaten hat — der Uebergang ist deshalb nicht sichtbar. */}
<div className="absolute inset-0"> <div className="motion-drift absolute inset-0 animate-[wh-drift_26s_ease-in-out_infinite_alternate]">
<ContentImage <ContentImage
src={image} src={image}
alt="" alt=""
@@ -94,15 +95,19 @@ export function Hero({
/> />
</div> </div>
) : ( ) : (
<div className="relative h-[82vh] min-h-[560px] w-full overflow-hidden">
<div className="motion-drift absolute inset-0 animate-[wh-drift_26s_ease-in-out_infinite_alternate]">
<ContentImage <ContentImage
src={image} src={image}
alt="" alt=""
tone="forest" tone="forest"
className="h-[78vh] min-h-[520px] w-full" className="h-full w-full"
label="Bienenstand im Morgenlicht · Altenmünster" label="Bienenstand im Morgenlicht · Altenmünster"
rounded={false} rounded={false}
priority priority
/> />
</div>
</div>
)} )}
{/* Gleichmaessige Abdunklung ueber die gesamte Flaeche — noetig, weil {/* Gleichmaessige Abdunklung ueber die gesamte Flaeche — noetig, weil
redaktionell gepflegtes Bild-/Videomaterial (z.B. helle Etiketten, redaktionell gepflegtes Bild-/Videomaterial (z.B. helle Etiketten,
@@ -110,6 +115,8 @@ export function Hero({
unlesbar machen kann. Der gerichtete Verlauf darunter verstaerkt das unlesbar machen kann. Der gerichtete Verlauf darunter verstaerkt das
gezielt dort, wo die Textspalte tatsaechlich sitzt (unten links). */} gezielt dort, wo die Textspalte tatsaechlich sitzt (unten links). */}
<div className="absolute inset-0 bg-forest/25" /> <div className="absolute inset-0 bg-forest/25" />
{/* Wabenraster ueber der rechten Bildhaelfte — die Marke im ersten Blick. */}
<HoneycombPattern className="text-honey-pale/[0.12] hidden md:block" size={34} fade="right" />
{/* Senkrecht auf schmalen Schirmen, waagerecht ab md. {/* Senkrecht auf schmalen Schirmen, waagerecht ab md.
Der frueher hier stehende to-tr-Verlauf liess die Textspalte auf Der frueher hier stehende to-tr-Verlauf liess die Textspalte auf
@@ -127,7 +134,7 @@ export function Hero({
<div className="absolute inset-0 flex items-center bg-gradient-to-t from-forest/85 via-forest/70 to-forest/10 md:bg-gradient-to-r md:from-forest/85 md:via-forest/70 md:to-transparent"> <div className="absolute inset-0 flex items-center bg-gradient-to-t from-forest/85 via-forest/70 to-forest/10 md:bg-gradient-to-r md:from-forest/85 md:via-forest/70 md:to-transparent">
<Container> <Container>
<motion.div <motion.div
className="max-w-xl text-cream" className="max-w-2xl text-cream"
initial={shouldReduceMotion ? false : "hidden"} initial={shouldReduceMotion ? false : "hidden"}
animate="visible" animate="visible"
variants={itemContainerVariants} variants={itemContainerVariants}
@@ -138,18 +145,23 @@ export function Hero({
> >
{location} {location}
</motion.p> </motion.p>
<motion.h1 data-reveal="" variants={itemVariants} className="text-4xl md:text-6xl leading-tight"> <motion.h1
data-reveal=""
variants={itemVariants}
className="text-5xl sm:text-6xl md:text-7xl lg:text-8xl leading-[0.95] tracking-[-0.02em]"
>
{t.footer.tagline} {t.footer.tagline}
</motion.h1> </motion.h1>
<motion.p data-reveal="" variants={itemVariants} className="mt-6 text-cream/85 max-w-md leading-relaxed"> <motion.p data-reveal="" variants={itemVariants} className="mt-8 text-lg text-cream/85 max-w-md leading-relaxed">
{intro} {intro}
</motion.p> </motion.p>
<motion.div data-reveal="" variants={itemVariants} className="mt-10 flex flex-wrap gap-4"> <motion.div data-reveal="" variants={itemVariants} className="mt-10 flex flex-wrap gap-4">
<LinkButton href="/shop">{t.home.hero.ctaPrimary}</LinkButton> <LinkButton href="/shop" variant="accent">
{t.home.hero.ctaPrimary}
</LinkButton>
<LinkButton <LinkButton
href="/imkerei" href="/imkerei"
variant="secondary" variant="inverted"
className="!border-cream !text-cream hover:!bg-cream hover:!text-forest"
> >
{t.home.hero.ctaSecondary} {t.home.hero.ctaSecondary}
</LinkButton> </LinkButton>
@@ -160,6 +172,14 @@ export function Hero({
</motion.div> </motion.div>
</Container> </Container>
</div> </div>
{/* Scroll-Hinweis: ein Tropfen loest sich aus einer Wabe. Rein
dekorativ; bei reduzierter Bewegung steht er still. */}
<div aria-hidden="true" className="pointer-events-none absolute bottom-8 left-1/2 hidden -translate-x-1/2 flex-col items-center md:flex">
<svg viewBox="0 0 24 26" className="h-6 w-6 text-honey-pale/80" fill="none">
<polygon points="12,1 22.4,7 22.4,19 12,25 1.6,19 1.6,7" stroke="currentColor" strokeWidth="1.2" />
</svg>
<span className="motion-drip -mt-1 block h-2.5 w-2 rounded-b-full rounded-t-[40%] bg-honey animate-[wh-drip_2.6s_var(--ease-arrive)_infinite]" />
</div>
</section> </section>
); );
} }
+24 -17
View File
@@ -1,4 +1,5 @@
import { Container, SectionHeading, LinkButton } from "@/components/ui"; import { Container, SectionHeading, LinkButton, FramedMedia } from "@/components/ui";
import { HoneycombPattern, HexMark } from "@/components/honeycomb";
import { ScrollReveal } from "@/components/scroll-reveal"; import { ScrollReveal } from "@/components/scroll-reveal";
import { ContentImage } from "@/components/content-image"; import { ContentImage } from "@/components/content-image";
import { ProductCard, TasteScale, AddToCartButton } from "@/components/product-card"; import { ProductCard, TasteScale, AddToCartButton } from "@/components/product-card";
@@ -55,9 +56,10 @@ export async function OriginStory() {
); );
const facts = parseListSimple(content["home.originStory.facts"]); const facts = parseListSimple(content["home.originStory.facts"]);
return ( return (
<section className="py-24 bg-cream/60"> <section className="py-28 bg-cream/60">
<Container className="grid md:grid-cols-2 gap-12 items-center"> <Container className="grid md:grid-cols-2 gap-16 lg:gap-24 items-center">
<ScrollReveal> <ScrollReveal>
<FramedMedia>
<ContentImage <ContentImage
src={content["home.originStory.image"] || null} src={content["home.originStory.image"] || null}
alt="" alt=""
@@ -66,6 +68,7 @@ export async function OriginStory() {
label={t.home.originStory.eyebrow} label={t.home.originStory.eyebrow}
sizes="(min-width: 768px) 50vw, 100vw" sizes="(min-width: 768px) 50vw, 100vw"
/> />
</FramedMedia>
</ScrollReveal> </ScrollReveal>
<ScrollReveal delay={0.1}> <ScrollReveal delay={0.1}>
<SectionHeading <SectionHeading
@@ -74,9 +77,12 @@ export async function OriginStory() {
/> />
<p className="mt-6 text-ink/75 leading-relaxed">{content["home.originStory.paragraph1"]}</p> <p className="mt-6 text-ink/75 leading-relaxed">{content["home.originStory.paragraph1"]}</p>
<p className="mt-4 text-ink/75 leading-relaxed">{content["home.originStory.paragraph2"]}</p> <p className="mt-4 text-ink/75 leading-relaxed">{content["home.originStory.paragraph2"]}</p>
<ul className="mt-6 space-y-1 text-sm text-ink/70"> <ul className="mt-8 grid gap-2.5 text-sm text-ink/75 sm:grid-cols-2">
{facts.map((fact) => ( {facts.map((fact) => (
<li key={fact}>{fact}</li> <li key={fact} className="flex items-start gap-2.5">
<HexMark className="mt-1.5 h-2 w-2 flex-shrink-0 text-honey" />
{fact}
</li>
))} ))}
</ul> </ul>
<LinkButton href="/herkunft" variant="secondary" className="mt-8"> <LinkButton href="/herkunft" variant="secondary" className="mt-8">
@@ -108,11 +114,10 @@ export async function Philosophy() {
{items.map((item, i) => ( {items.map((item, i) => (
<ScrollReveal key={item.title} delay={i * 0.08}> <ScrollReveal key={item.title} delay={i * 0.08}>
<div className={`border-t border-ink/15 pt-6 ${i % 2 === 1 ? "lg:mt-10" : ""}`}> <div className={`border-t border-ink/15 pt-6 ${i % 2 === 1 ? "lg:mt-10" : ""}`}>
<span className="font-serif text-3xl text-honey-dark"> {/* Wabe statt "01"-Nummer: die vier Punkte sind gleichrangig, keine Reihenfolge. */}
{String(i + 1).padStart(2, "0")} <HexMark filled={i % 2 === 0} className="h-5 w-5 text-honey" />
</span> <h3 className="mt-4 text-2xl text-forest">{item.title}</h3>
<h3 className="mt-3 text-lg text-forest">{item.title}</h3> <p className="mt-3 text-sm text-ink/70 leading-relaxed">
<p className="mt-2 text-sm text-ink/65 leading-relaxed">
{item.text} {item.text}
</p> </p>
</div> </div>
@@ -133,8 +138,9 @@ export async function ProductSpotlight({ product }: { product: HoneySpotlightPro
const locale = await getLocale(); const locale = await getLocale();
const t = getDictionary(locale); const t = getDictionary(locale);
return ( return (
<section className="py-24 bg-forest text-cream"> <section className="relative overflow-hidden py-28 bg-forest text-cream">
<Container className="grid md:grid-cols-2 gap-14 items-center"> <HoneycombPattern className="text-honey-light/[0.08]" size={28} fade="right" />
<Container className="relative grid md:grid-cols-2 gap-14 lg:gap-24 items-center">
<ScrollReveal> <ScrollReveal>
<ContentImage <ContentImage
src={product.images[0] ?? null} src={product.images[0] ?? null}
@@ -149,7 +155,7 @@ export async function ProductSpotlight({ product }: { product: HoneySpotlightPro
<p className="tracking-wide-caps text-xs text-honey-light mb-3"> <p className="tracking-wide-caps text-xs text-honey-light mb-3">
{product.harvest.type} {product.harvest.year} {product.harvest.type} {product.harvest.year}
</p> </p>
<h2 className="text-3xl md:text-4xl">{product.name}</h2> <h2 className="text-5xl md:text-6xl leading-[1] tracking-[-0.015em]">{product.name}</h2>
<p className="mt-4 text-cream/75 leading-relaxed"> <p className="mt-4 text-cream/75 leading-relaxed">
{t.home.productSpotlight.description} {t.home.productSpotlight.description}
</p> </p>
@@ -178,19 +184,20 @@ export async function ProductSpotlight({ product }: { product: HoneySpotlightPro
</div> </div>
<div> <div>
<dt className="text-cream/65">{t.home.productSpotlight.price}</dt> <dt className="text-cream/65">{t.home.productSpotlight.price}</dt>
<dd>{formatPrice(product.priceCents, product.currency, locale)}</dd> <dd className="font-serif text-2xl text-cream tabular-nums">
{formatPrice(product.priceCents, product.currency, locale)}
</dd>
</div> </div>
</dl> </dl>
<div className="mt-10 flex flex-wrap gap-4"> <div className="mt-10 flex flex-wrap gap-4">
<LinkButton <LinkButton
href={`/honig/${product.slug}`} href={`/honig/${product.slug}`}
variant="secondary" variant="inverted"
className="!border-cream !text-cream hover:!bg-cream hover:!text-forest"
> >
{t.home.productSpotlight.cta} {t.home.productSpotlight.cta}
</LinkButton> </LinkButton>
<AddToCartButton product={product} /> <AddToCartButton product={product} variant="accent" />
</div> </div>
</ScrollReveal> </ScrollReveal>
</Container> </Container>
+41 -41
View File
@@ -1,4 +1,6 @@
import { Container, SectionHeading, LinkButton } from "@/components/ui"; import { Container, SectionHeading, LinkButton, FramedMedia } from "@/components/ui";
import { ProcessPath } from "@/components/process-path";
import { HoneycombPattern, HexMark } from "@/components/honeycomb";
import { ScrollReveal } from "@/components/scroll-reveal"; import { ScrollReveal } from "@/components/scroll-reveal";
import { ContentImage } from "@/components/content-image"; import { ContentImage } from "@/components/content-image";
import { StarRating } from "@/components/star-rating"; import { StarRating } from "@/components/star-rating";
@@ -23,21 +25,7 @@ export async function ProcessTimeline() {
className="mx-auto" className="mx-auto"
/> />
</ScrollReveal> </ScrollReveal>
<div className="mt-16 grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-8"> <ProcessPath steps={steps} />
{steps.map((step, i) => (
<ScrollReveal key={step.number} delay={i * 0.06}>
<div>
<span className="text-honey-dark text-sm tracking-wide-caps">
{step.number}
</span>
<h3 className="mt-2 text-lg text-forest">{step.title}</h3>
<p className="mt-2 text-xs text-ink/70 leading-relaxed">
{step.text}
</p>
</div>
</ScrollReveal>
))}
</div>
</Container> </Container>
</section> </section>
); );
@@ -56,7 +44,7 @@ export async function RegionalSection() {
); );
return ( return (
<section className="py-24"> <section className="py-24">
<Container className="grid md:grid-cols-2 gap-12 items-center"> <Container className="grid md:grid-cols-2 gap-16 lg:gap-24 items-center">
<ScrollReveal className="order-2 md:order-1"> <ScrollReveal className="order-2 md:order-1">
<SectionHeading <SectionHeading
eyebrow={t.home.regionalSection.eyebrow} eyebrow={t.home.regionalSection.eyebrow}
@@ -74,6 +62,7 @@ export async function RegionalSection() {
</div> </div>
</ScrollReveal> </ScrollReveal>
<ScrollReveal className="order-1 md:order-2" delay={0.1}> <ScrollReveal className="order-1 md:order-2" delay={0.1}>
<FramedMedia side="left">
<ContentImage <ContentImage
src={content["home.regionalSection.image"] || null} src={content["home.regionalSection.image"] || null}
alt="" alt=""
@@ -82,6 +71,7 @@ export async function RegionalSection() {
label={t.home.regionalSection.eyebrow} label={t.home.regionalSection.eyebrow}
sizes="(min-width: 768px) 50vw, 100vw" sizes="(min-width: 768px) 50vw, 100vw"
/> />
</FramedMedia>
</ScrollReveal> </ScrollReveal>
</Container> </Container>
</section> </section>
@@ -94,26 +84,30 @@ export async function Sustainability() {
const content = await getContentValues(["home.sustainability.items"], locale); const content = await getContentValues(["home.sustainability.items"], locale);
const items = parseListTitled(content["home.sustainability.items"]); const items = parseListTitled(content["home.sustainability.items"]);
return ( return (
<section className="py-24 bg-cream/60"> <section className="py-28 bg-cream/60">
<Container> {/* Geteilt statt Viererreihe: die Ueberschrift laeuft mit, die Punkte
<ScrollReveal> werden als ruhige Liste gelesen. */}
<SectionHeading <Container className="grid gap-12 lg:grid-cols-12 lg:gap-16">
eyebrow={t.home.sustainability.eyebrow} <ScrollReveal className="lg:col-span-5">
title={t.home.sustainability.title} <div className="lg:sticky lg:top-32">
align="center" <SectionHeading eyebrow={t.home.sustainability.eyebrow} title={t.home.sustainability.title} />
className="mx-auto"
/>
</ScrollReveal>
<div className="mt-14 grid sm:grid-cols-2 lg:grid-cols-4 gap-8">
{items.map((item, i) => (
<ScrollReveal key={item.title} delay={i * 0.08}>
<h3 className="text-lg text-forest">{item.title}</h3>
<p className="mt-2 text-sm text-ink/65 leading-relaxed">
{item.text}
</p>
</ScrollReveal>
))}
</div> </div>
</ScrollReveal>
<ul className="divide-y divide-ink/10 border-y border-ink/10 lg:col-span-7">
{items.map((item, i) => (
<li key={item.title}>
<ScrollReveal delay={i * 0.08}>
<div className="group grid grid-cols-[auto_1fr] gap-x-5 py-7">
<HexMark className="mt-2 h-3 w-3 text-honey transition-transform duration-500 ease-[var(--ease-arrive)] group-hover:rotate-90" />
<div>
<h3 className="text-2xl text-forest">{item.title}</h3>
<p className="mt-2 max-w-[55ch] text-ink/70 leading-relaxed">{item.text}</p>
</div>
</div>
</ScrollReveal>
</li>
))}
</ul>
</Container> </Container>
</section> </section>
); );
@@ -128,8 +122,9 @@ export async function BusinessTeaser() {
); );
return ( return (
<section className="py-24"> <section className="py-24">
<Container className="grid md:grid-cols-2 gap-12 items-center"> <Container className="grid md:grid-cols-2 gap-16 lg:gap-24 items-center">
<ScrollReveal> <ScrollReveal>
<FramedMedia>
<ContentImage <ContentImage
src={content["home.businessTeaser.image"] || null} src={content["home.businessTeaser.image"] || null}
alt="" alt=""
@@ -138,6 +133,7 @@ export async function BusinessTeaser() {
label={t.home.businessTeaser.eyebrow} label={t.home.businessTeaser.eyebrow}
sizes="(min-width: 768px) 50vw, 100vw" sizes="(min-width: 768px) 50vw, 100vw"
/> />
</FramedMedia>
</ScrollReveal> </ScrollReveal>
<ScrollReveal delay={0.1}> <ScrollReveal delay={0.1}>
<SectionHeading <SectionHeading
@@ -173,8 +169,9 @@ export async function Reviews() {
: t.home.reviews.testimonials.map((r) => ({ ...r, rating: 5, authorUrl: null })); : t.home.reviews.testimonials.map((r) => ({ ...r, rating: 5, authorUrl: null }));
return ( return (
<section className="py-24 bg-forest text-cream"> <section className="relative overflow-hidden py-28 bg-forest text-cream">
<Container> <HoneycombPattern className="text-honey-light/[0.07]" size={30} />
<Container className="relative">
<ScrollReveal> <ScrollReveal>
<SectionHeading <SectionHeading
eyebrow={t.home.reviews.eyebrow} eyebrow={t.home.reviews.eyebrow}
@@ -192,10 +189,13 @@ export async function Reviews() {
<div className="mt-14 grid md:grid-cols-3 gap-10"> <div className="mt-14 grid md:grid-cols-3 gap-10">
{items.slice(0, 6).map((review, i) => ( {items.slice(0, 6).map((review, i) => (
<ScrollReveal key={`${review.author}-${i}`} delay={i * 0.08}> <ScrollReveal key={`${review.author}-${i}`} delay={i * 0.08}>
<span aria-hidden="true" className="block font-serif text-7xl leading-[0.6] text-honey-light/70">
„
</span>
<StarRating value={review.rating} /> <StarRating value={review.rating} />
<span className="sr-only">{t.home.reviews.starsOutOf5(review.rating)}</span> <span className="sr-only">{t.home.reviews.starsOutOf5(review.rating)}</span>
<p className="mt-4 text-cream/85 leading-relaxed italic"> <p className="mt-4 font-serif text-xl text-cream/90 leading-relaxed italic">
„{review.quote}“ {review.quote}
</p> </p>
{/* Zuordnung zur Quelle: die Nutzungsbedingungen der Places API {/* Zuordnung zur Quelle: die Nutzungsbedingungen der Places API
verlangen, dass eine angezeigte Rezension zu Google verlangen, dass eine angezeigte Rezension zu Google
+95
View File
@@ -0,0 +1,95 @@
"use client";
import { useRef } from "react";
import { motion, useReducedMotion, useScroll, useSpring, useTransform, type MotionValue } from "motion/react";
interface Step {
number: string;
title: string;
text: string;
}
/**
* "Vom Bienenstock ins Glas" als Weg, den der Honig nimmt: eine Honiglinie
* fuellt sich im Takt des Scrollens und laesst die Waben der einzelnen
* Schritte der Reihe nach aufleuchten. Hier traegt die Bewegung Bedeutung —
* die Reihenfolge IST der Inhalt.
*
* Ab lg waagerecht (sechs Spalten), darunter senkrecht mit Linie links.
* Bei reduzierter Bewegung ist der Weg von Anfang an gefuellt.
*/
export function ProcessPath({ steps }: { steps: Step[] }) {
const ref = useRef<HTMLOListElement>(null);
const reduceMotion = useReducedMotion();
const { scrollYProgress } = useScroll({ target: ref, offset: ["start 85%", "end 55%"] });
// Gefedert statt direkt am Scrollwert: die Linie "fliesst" nach, wie Honig.
const smooth = useSpring(scrollYProgress, { stiffness: 90, damping: 22, mass: 0.6 });
const full = useTransform(smooth, () => 1);
const progress = reduceMotion ? full : smooth;
return (
<ol ref={ref} className="relative mt-16 grid gap-10 pl-10 lg:grid-cols-6 lg:gap-8 lg:pl-0 lg:pt-12">
{/* Leitung (Grundlinie) und Honigfuellung — waagerecht ab lg, sonst senkrecht. */}
<span aria-hidden="true" className="absolute left-[11px] top-2 bottom-2 w-px bg-ink/15 lg:hidden" />
<motion.span
aria-hidden="true"
style={{ scaleY: progress }}
className="absolute left-[10px] top-2 bottom-2 w-[3px] origin-top rounded-full bg-honey lg:hidden"
/>
<span aria-hidden="true" className="absolute left-3 right-[calc((100%_-_10rem)/6_-_12px)] top-[11px] hidden h-px bg-ink/15 lg:block" />
<motion.span
aria-hidden="true"
style={{ scaleX: progress }}
className="absolute left-3 right-[calc((100%_-_10rem)/6_-_12px)] top-[10px] hidden h-[3px] origin-left rounded-full bg-honey lg:block"
/>
{steps.map((step, i) => (
<ProcessStep key={step.number} step={step} index={i} total={steps.length} progress={progress} />
))}
</ol>
);
}
function ProcessStep({
step,
index,
total,
progress,
}: {
step: Step;
index: number;
total: number;
progress: MotionValue<number>;
}) {
// Die Wabe "entzuendet" sich, sobald die Linie ihre Position erreicht.
const at = total > 1 ? index / (total - 1) : 0;
const lit = useTransform(progress, [Math.max(0, at - 0.06), at + 0.02], [0, 1]);
const scale = useTransform(lit, [0, 1], [0.85, 1]);
return (
<li className="relative">
<motion.span
aria-hidden="true"
style={{ scale }}
className="absolute -left-10 top-0 flex h-6 w-6 items-center justify-center lg:-top-12 lg:left-0"
>
<svg viewBox="0 0 24 26" className="h-6 w-6">
<polygon
points="12,1 22.4,7 22.4,19 12,25 1.6,19 1.6,7"
fill="var(--color-background)"
stroke="var(--color-honey-dark)"
strokeWidth="1.5"
/>
<motion.polygon
points="12,4.5 19.4,8.75 19.4,17.25 12,21.5 4.6,17.25 4.6,8.75"
fill="var(--color-honey)"
style={{ opacity: lit }}
/>
</svg>
</motion.span>
<span className="font-serif text-sm text-honey-dark tabular-nums">{step.number}</span>
<h3 className="mt-1 text-xl text-forest">{step.title}</h3>
<p className="mt-2 text-sm text-ink/70 leading-relaxed">{step.text}</p>
</li>
);
}
+1 -1
View File
@@ -160,7 +160,7 @@ export function AddToCartButton({
}: { }: {
product: ProductWithRelations; product: ProductWithRelations;
quantity?: number; quantity?: number;
variant?: "primary" | "secondary" | "ghost"; variant?: "primary" | "secondary" | "ghost" | "inverted" | "accent";
className?: string; className?: string;
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
+2 -2
View File
@@ -419,8 +419,8 @@ export function Header({ companyName }: { companyName: string }) {
<Container className="pb-10"> <Container className="pb-10">
<LinkButton <LinkButton
href="/shop" href="/shop"
variant="secondary" variant="inverted"
className="!border-cream !text-cream hover:!bg-cream hover:!text-forest w-full" className="w-full"
onClick={() => setMobileOpen(false)} onClick={() => setMobileOpen(false)}
> >
{t.nav.discoverHoney} {t.nav.discoverHoney}
+36 -1
View File
@@ -18,7 +18,7 @@ export function Container({
); );
} }
type ButtonVariant = "primary" | "secondary" | "ghost"; type ButtonVariant = "primary" | "secondary" | "ghost" | "inverted" | "accent";
// Signatur-Hover: der Knopf fuellt sich von unten wie ein Glas — beim // Signatur-Hover: der Knopf fuellt sich von unten wie ein Glas — beim
// Primaerknopf mit Honig, beim Sekundaerknopf mit Waldgruen. Umgesetzt als // Primaerknopf mit Honig, beim Sekundaerknopf mit Waldgruen. Umgesetzt als
@@ -31,6 +31,11 @@ const buttonStyles: Record<ButtonVariant, string> = {
primary: `${fillBase} bg-forest bg-[linear-gradient(var(--color-honey-dark),var(--color-honey-dark))] text-cream border border-forest hover:border-honey-dark`, primary: `${fillBase} bg-forest bg-[linear-gradient(var(--color-honey-dark),var(--color-honey-dark))] text-cream border border-forest hover:border-honey-dark`,
secondary: `${fillBase} bg-transparent bg-[linear-gradient(var(--color-forest),var(--color-forest))] text-forest border border-forest hover:text-cream focus-visible:text-cream`, secondary: `${fillBase} bg-transparent bg-[linear-gradient(var(--color-forest),var(--color-forest))] text-forest border border-forest hover:text-cream focus-visible:text-cream`,
ghost: "bg-transparent text-ink border border-ink/20 hover:border-ink hover:bg-ink/[0.03]", ghost: "bg-transparent text-ink border border-ink/20 hover:border-ink hover:bg-ink/[0.03]",
// Fuer dunkle Flaechen (Hero, Waldgruen-Abschnitte, Menue): Creme-Rahmen,
// fuellt sich mit Creme, Text wechselt auf Waldgruen.
inverted: `${fillBase} bg-transparent bg-[linear-gradient(var(--color-cream),var(--color-cream))] text-cream border border-cream hover:text-forest focus-visible:text-forest`,
// Hauptaktion auf dunklen Flaechen, wo Waldgruen im Grund verschwinden wuerde.
accent: `${fillBase} bg-honey-dark bg-[linear-gradient(var(--color-cream),var(--color-cream))] text-cream border border-honey-dark hover:text-forest hover:border-cream focus-visible:text-forest`,
}; };
const buttonBase = const buttonBase =
@@ -129,3 +134,33 @@ export function SectionHeading({
</div> </div>
); );
} }
/**
* Bild mit versetztem Honigrahmen dahinter — gibt den Bildspalten der
* Startseite Tiefe, statt sie flach neben den Text zu legen. Der Rahmen
* weicht beim Hover etwas zurueck, das Bild bleibt ruhig.
*/
export function FramedMedia({
children,
side = "right",
className = "",
}: {
children: ReactNode;
/** Richtung, in die der Rahmen versetzt ist. */
side?: "left" | "right";
className?: string;
}) {
return (
<div className={`group/frame relative isolate ${className}`}>
<span
aria-hidden="true"
className={`absolute inset-0 -z-10 border border-honey/60 transition-transform duration-700 ease-[var(--ease-arrive)] ${
side === "right"
? "translate-x-4 translate-y-4 group-hover/frame:translate-x-6 group-hover/frame:translate-y-6"
: "-translate-x-4 translate-y-4 group-hover/frame:-translate-x-6 group-hover/frame:translate-y-6"
}`}
/>
{children}
</div>
);
}