QR Web
Handy ansicht und website ansicht für den Qr gemacht
This commit is contained in:
1 parent
9725e0229d
commit
c09f674653
15 files changed
+639
-103
No files matched your search
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useLayoutEffect, useRef } from "react";
|
||||
import { gsap, EASE, MM } from "@/lib/gsap";
|
||||
import { gsap, ScrollTrigger, EASE, MM } from "@/lib/gsap";
|
||||
|
||||
/**
|
||||
* 1 px `--accent` line directly under the header (the header writes
|
||||
@@ -9,34 +9,122 @@ import { gsap, EASE, MM } from "@/lib/gsap";
|
||||
*
|
||||
* Scrubbed by ScrollTrigger, not by a second animation runtime: this was the
|
||||
* only reason `useScroll` (framer-motion) shipped on every route.
|
||||
*
|
||||
* ---------------------------------------------------------------------------
|
||||
* Reduziert-Fähnchen
|
||||
*
|
||||
* Trägt eine Seite einen Gutschein, hängt an der Spitze des Balkens ein rotes
|
||||
* Fähnchen mit dem Nachlass – dort, wo der Leser seine eigene Position abliest.
|
||||
* Es blendet sich erst ein, wenn der Abschnitt mit dem Formular ins Bild kommt:
|
||||
* Bis dahin sagt der Balken „so weit bist du“, ab dort zusätzlich „und hier wird
|
||||
* er eingelöst“.
|
||||
*
|
||||
* Diese Komponente hängt in <Shell/> und läuft auf JEDER Seite – auf dem
|
||||
* Impressum darf kein „−20 %“ kleben. Statt einen Zustand durch das Layout zu
|
||||
* fädeln (was der App-Router nicht hergibt), liest sie den Text aus dem DOM:
|
||||
* <ProcessContact/> schreibt ihn als `data-sale` an den Kontaktabschnitt, wenn
|
||||
* ein Gutschein übergeben wurde. Denselben Weg geht schon die Regel in
|
||||
* globals.css, die den Kopf-CTA auf der Messeseite ausblendet.
|
||||
*/
|
||||
export function ScrollProgress() {
|
||||
const bar = useRef<HTMLDivElement>(null);
|
||||
const tag = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const el = bar.current;
|
||||
if (!el) return;
|
||||
const mm = gsap.matchMedia();
|
||||
mm.add({ reduce: MM.reduce, ok: MM.ok }, () => {
|
||||
mm.add({ reduce: MM.reduce, ok: MM.ok }, (ctx) => {
|
||||
/* Reduced motion too: the bar reports position, it does not animate for effect –
|
||||
the scrub follows the reader's own scrolling either way. */
|
||||
/* Die Strecke wird gemessen, nicht aus einem Element abgeleitet.
|
||||
Vorher stand hier `trigger: document.documentElement` mit „top top“ bis
|
||||
„bottom bottom“. <html> trägt aber `h-full`, ist also genau einen Bildschirm
|
||||
hoch – Anfang und Ende fielen zusammen, die Strecke war null und der Balken
|
||||
blieb auf 0. Auf dem Desktop fiel das nicht auf, weil Lenis dort seine eigene
|
||||
Position meldet; auf Mobil ist Lenis aus (SmoothScroll.tsx überlässt Touch dem
|
||||
nativen Scrollen), und der Balken zeichnete überhaupt nicht.
|
||||
`maxScroll` fragt den tatsächlichen Scrollweg ab, `invalidateOnRefresh` rechnet
|
||||
ihn neu, wenn die Seite ihre Höhe ändert. */
|
||||
const scrub = {
|
||||
start: 0,
|
||||
end: () => ScrollTrigger.maxScroll(window),
|
||||
scrub: true,
|
||||
invalidateOnRefresh: true,
|
||||
} as const;
|
||||
|
||||
gsap.set(el, { scaleX: 0, transformOrigin: "0 50%" });
|
||||
gsap.to(el, {
|
||||
scaleX: 1,
|
||||
ease: EASE.none,
|
||||
scrollTrigger: { trigger: document.documentElement, start: "top top", end: "bottom bottom", scrub: true },
|
||||
gsap.to(el, { scaleX: 1, ease: EASE.none, scrollTrigger: scrub });
|
||||
|
||||
/* Das Fähnchen steht immer im Dokument und wird hier beschriftet, statt seinen
|
||||
Text durch React-State zu schicken: Ein `setState` im Effekt wäre ein zweiter
|
||||
Durchlauf für eine Angabe, die sich nach dem ersten Rendern nie mehr ändert
|
||||
(und react-hooks verbietet es zu Recht). Ohne `data-sale` bleibt es leer und
|
||||
durchsichtig – auf dem Impressum klebt kein „−20 %“. */
|
||||
const flag = tag.current;
|
||||
const anchor = document.querySelector<HTMLElement>("main [data-sale]");
|
||||
if (!flag || !anchor?.dataset.sale) return;
|
||||
const [value, stub] = flag.querySelectorAll<HTMLElement>("[data-cell]");
|
||||
value.textContent = anchor.dataset.sale;
|
||||
stub.textContent = anchor.dataset.saleStub ?? "";
|
||||
|
||||
/* Der Balken selbst wird über `scaleX` gezogen – ein Kind darin würde
|
||||
mitgestaucht. Das Fähnchen läuft deshalb eigenständig über dieselbe
|
||||
Strecke; `xPercent: -100` legt seine rechte Kante auf die Spitze, damit
|
||||
es am Ende nicht über den Fensterrand hinausragt. Der Endwert ist eine
|
||||
Funktion, `invalidateOnRefresh` rechnet ihn beim Größenwechsel neu. */
|
||||
gsap.set(flag, { opacity: 0, xPercent: -100 });
|
||||
gsap.fromTo(
|
||||
flag,
|
||||
{ x: 0 },
|
||||
{ x: () => window.innerWidth, ease: EASE.none, scrollTrigger: { ...scrub } },
|
||||
);
|
||||
|
||||
/* Nur `opacity` – nichts, was Fokus oder Tab-Reihenfolge anfasst. Bei
|
||||
reduzierter Bewegung ohne Überblendung, aber mit demselben Ergebnis. */
|
||||
const fade = ctx.conditions?.reduce ? 0 : 0.4;
|
||||
ScrollTrigger.create({
|
||||
trigger: anchor,
|
||||
start: "top 90%",
|
||||
onEnter: () => gsap.to(flag, { opacity: 1, duration: fade, ease: EASE.out }),
|
||||
onLeaveBack: () => gsap.to(flag, { opacity: 0, duration: fade, ease: EASE.out }),
|
||||
});
|
||||
});
|
||||
return () => mm.revert();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={bar}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none fixed inset-x-0 z-50 h-px origin-left scale-x-0 bg-accent transition-opacity duration-300 [[data-menu-open]_&]:opacity-0"
|
||||
style={{ top: "var(--header-h, 60px)" }}
|
||||
/>
|
||||
<>
|
||||
<div
|
||||
ref={bar}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none fixed inset-x-0 z-50 h-px origin-left scale-x-0 bg-accent transition-opacity duration-300 [[data-menu-open]_&]:opacity-0"
|
||||
style={{ top: "var(--header-h, 60px)" }}
|
||||
/>
|
||||
{/* Ein Gutschein im Kleinstformat: runde Ecken wie der große, und quer durch die
|
||||
Mitte dieselbe Reißkante – hier hochkant, weil das Fähnchen breiter als hoch
|
||||
ist. Rechts hinter der Perforation sitzt der Abriss.
|
||||
|
||||
Anders als beim großen Gutschein sind es keine gestanzten Kerben, sondern nur
|
||||
die gestrichelte Linie: Eine Kerbe muss den Grund dahinter zeigen, und dieses
|
||||
Fähnchen schwebt über der Seite – mal über Tinte, mal über Elfenbein. Eine
|
||||
Kerbe in fester Farbe wäre auf einem von beiden Gründen ein Fleck.
|
||||
|
||||
Rot, weil Rot reduziert heißt und Gold das nicht sagen kann. `aria-hidden` wie
|
||||
der Balken: Dass der Vorteil an der Anfrage hängt, steht als Text im Formular
|
||||
darunter, nicht nur in dieser Farbe. Leer und durchsichtig, solange die Seite
|
||||
keinen Gutschein trägt. */}
|
||||
<div
|
||||
ref={tag}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none fixed left-0 z-50 flex -translate-y-1/2 items-stretch overflow-hidden rounded-[5px] bg-sale font-mono text-[10px] uppercase leading-none tracking-[0.12em] text-ivory opacity-0 [[data-menu-open]_&]:!opacity-0"
|
||||
style={{ top: "var(--header-h, 60px)" }}
|
||||
>
|
||||
<span data-cell="" className="px-2 py-[5px]" />
|
||||
<span className="border-l border-dashed border-ivory/55" />
|
||||
<span data-cell="" className="px-[7px] py-[5px]" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user