moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
'use client';
import {
useCallback,
useEffect,
useRef,
useState,
type CSSProperties,
type ReactNode,
} from 'react';
interface RevealProps {
readonly children: ReactNode;
/** Stagger slot 15 → transition-delay 60ms steps (see globals.css). */
readonly delay?: 1 | 2 | 3 | 4 | 5;
readonly as?: 'div' | 'section' | 'li' | 'span';
readonly className?: string;
readonly style?: CSSProperties;
}
/**
* Scroll-reveal wrapper: fades + lifts children in when they enter the
* viewport. Purely presentational — content is in the DOM at SSR (SEO-safe)
* and `prefers-reduced-motion` disables the effect entirely via globals.css.
*/
export default function Reveal({
children,
delay,
as: Tag = 'div',
className,
style,
}: RevealProps) {
const nodeRef = useRef<HTMLElement | null>(null);
const [visible, setVisible] = useState(false);
const setNode = useCallback((node: HTMLElement | null) => {
nodeRef.current = node;
}, []);
useEffect(() => {
const node = nodeRef.current;
if (!node) return;
if (typeof IntersectionObserver === 'undefined') {
setVisible(true);
return;
}
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
setVisible(true);
observer.disconnect();
}
}
},
{ rootMargin: '0px 0px -10% 0px', threshold: 0.1 },
);
observer.observe(node);
// Safety net: if a section is never scrolled into view — a crawler, a
// social-preview renderer, or a full-page screenshot that paints without
// scrolling — the observer never fires and the content would stay stuck at
// opacity:0. Reveal it anyway shortly after mount so no section is ever a
// headline floating in an empty void. Real users scrolling normally still
// trip the observer first and get the entrance animation per section.
const fallback = window.setTimeout(() => setVisible(true), 900);
return () => {
observer.disconnect();
window.clearTimeout(fallback);
};
}, []);
const classes = [
'reveal',
visible ? 'is-visible' : '',
delay ? `reveal-d${delay}` : '',
className ?? '',
]
.filter(Boolean)
.join(' ');
return (
<Tag ref={setNode} className={classes} style={style}>
{children}
</Tag>
);
}