first publish

This commit is contained in:
knso committed 2026-09-06 17:21:52 +02:00
commit b8e41b4d55
147 files changed
+19367

No files matched your search

+107
View File
@@ -0,0 +1,107 @@
"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 { 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");
router.push(href, { scroll: false });
window.clearTimeout(timer.current);
timer.current = window.setTimeout(() => {
if (phase.current === "pushed") window.location.assign(href);
}, 900);
},
});
});
}, [router, 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;