Auf her-career.anelia-wolf.com liegt die Seite unter „/“. usePathname() meldet dort „/“, und counterpart() leitete daraus folgerichtig die englische STARTSEITE ab – wer am Stand gescannt und dann auf „EN“ getippt hat, landete auf anelia-wolf.com/en/. Der Umschalter fragt jetzt den hreflang-Verweis im Kopf des Dokuments: Der hängt an der Seite, nicht an der Adresse, unter der sie ausgeliefert wird. Am selben Wurzelproblem: Der Sprachwechsel ist wegen der zwei Wurzel-Layouts ohnehin ein neues Dokument. Den macht jetzt der Browser statt router.push(), und über die Sprachgrenze wird nicht mehr vorgeladen – auf der Subdomain scheiterte der Vorabruf an CORS, mehrfach pro Seitenaufbau. Dazu zwei Sachen am Kopf der Messeseite: Das Monogramm steht hinter dem Schlusspunkt der Begrüßung, 0,783 em hoch (die Oberlänge der Playfair Display, im Browser gemessen) und auf der Grundlinie – und dafür nicht mehr in der Kopfzeile, wo es sonst zweimal auf einem Bildschirm stünde. Es steht dort dekorativ, sonst wandert der Text seines <title> über SplitText ins aria-label der Überschrift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
4.2 KiB
TypeScript
116 lines
4.2 KiB
TypeScript
"use client";
|
||
|
||
import { usePathname, useRouter } from "next/navigation";
|
||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||
import { gsap, EASE } from "@/lib/gsap";
|
||
import { WolfMark } from "@/components/brand/WolfMark";
|
||
import { chrome as chromeDe, type Chrome } from "@/content/chrome";
|
||
import { crossesLangRoot } from "@/lib/i18n";
|
||
import { endTransition, setTransitionPhase, subscribe } from "./transition-store";
|
||
|
||
export interface CurtainProps {
|
||
/** The monogram (48 px). layout.tsx passes `<BrandMark/>`; falls back to `<WolfMark/>`. */
|
||
children?: ReactNode;
|
||
/** Texte der Hülle. Ohne Angabe die deutsche Fassung. */
|
||
chrome?: Chrome;
|
||
}
|
||
|
||
/**
|
||
* The page-turn sheet: fixed ivory, 1 px gold hairline on its top edge,
|
||
* monogram + destination label centred. Note the start state lives ONLY in
|
||
* GSAP (`yPercent`) – a CSS `translateY(100%)` would be read as the tween's
|
||
* base `y` and added on top, which pushed the whole cover tween one viewport
|
||
* below the fold (the reader saw a hard cut, then the sheet wiping up over the
|
||
* new page). Slides up 0.45 s (power3.inOut),
|
||
* pushes the route, keeps rising once the pathname changed, then resets.
|
||
* Hard fallback: full navigation when the route has not changed after 900 ms.
|
||
*/
|
||
export function Curtain({ children, chrome = chromeDe }: CurtainProps) {
|
||
const router = useRouter();
|
||
const pathname = usePathname();
|
||
const sheet = useRef<HTMLDivElement>(null);
|
||
const phase = useRef<"idle" | "cover" | "pushed" | "reveal">("idle");
|
||
const timer = useRef<number>(0);
|
||
const [label, setLabel] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
return subscribe((s) => {
|
||
if (s.phase !== "cover" || !s.href || phase.current !== "idle") return;
|
||
const el = sheet.current;
|
||
if (!el) return;
|
||
const href = s.href;
|
||
phase.current = "cover";
|
||
setLabel(s.label ?? chrome.curtain.fallbackLabel);
|
||
gsap.killTweensOf(el);
|
||
gsap.set(el, { y: 0, yPercent: 100, autoAlpha: 1, pointerEvents: "auto" });
|
||
gsap.to(el, {
|
||
yPercent: 0,
|
||
duration: 0.45,
|
||
ease: EASE.inOut,
|
||
onComplete: () => {
|
||
phase.current = "pushed";
|
||
setTransitionPhase("pushed");
|
||
/* Sprachwechsel: den Dokumentwechsel macht der Browser, nicht der Router
|
||
(Begründung in crossesLangRoot(), src/lib/i18n.ts). Die Hülle bleibt
|
||
liegen, bis das neue Dokument da ist – sie gehört zu diesem hier. */
|
||
if (crossesLangRoot(pathname, href)) {
|
||
window.location.assign(href);
|
||
return;
|
||
}
|
||
router.push(href, { scroll: false });
|
||
window.clearTimeout(timer.current);
|
||
timer.current = window.setTimeout(() => {
|
||
if (phase.current === "pushed") window.location.assign(href);
|
||
}, 900);
|
||
},
|
||
});
|
||
});
|
||
}, [router, pathname, chrome.curtain.fallbackLabel]);
|
||
|
||
// Route changed → the sheet continues upward and resets.
|
||
useEffect(() => {
|
||
if (phase.current !== "pushed") return;
|
||
const el = sheet.current;
|
||
if (!el) return;
|
||
window.clearTimeout(timer.current);
|
||
phase.current = "reveal";
|
||
setTransitionPhase("reveal");
|
||
gsap.to(el, {
|
||
yPercent: -100,
|
||
duration: 0.45,
|
||
ease: EASE.inOut,
|
||
delay: 0.05,
|
||
onComplete: () => {
|
||
gsap.set(el, { y: 0, yPercent: 100, autoAlpha: 0, pointerEvents: "none" });
|
||
phase.current = "idle";
|
||
setLabel(null);
|
||
endTransition();
|
||
},
|
||
});
|
||
}, [pathname]);
|
||
|
||
useEffect(() => () => window.clearTimeout(timer.current), []);
|
||
|
||
return (
|
||
<div
|
||
ref={sheet}
|
||
id="curtain"
|
||
aria-hidden="true"
|
||
data-theme="light"
|
||
className="pointer-events-none invisible fixed inset-0 z-[70] flex flex-col items-center justify-center gap-6 bg-ivory text-ink"
|
||
>
|
||
<span
|
||
aria-hidden="true"
|
||
className="absolute inset-x-0 top-0 h-px"
|
||
style={{ background: "var(--gold-hairline-light)" }}
|
||
/>
|
||
<span className="block h-12 text-ink [&_svg]:h-12 [&_svg]:w-auto">
|
||
{children ?? <WolfMark idPrefix="curtain" className="h-12 w-auto" />}
|
||
</span>
|
||
<span className="min-h-[1em] font-mono text-xs tracking-[0.06em] text-gold-deep">{label}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default Curtain;
|