reveal the editorial page top-to-bottom: wrap every block and cascade the first screenful
This commit is contained in:
parent
eb2956c3f9
commit
134bc9ede0
4 changed files with 74 additions and 51 deletions
|
|
@ -168,11 +168,13 @@ export default function EditorialSite() {
|
||||||
|
|
||||||
{/* ----------------------------------------------------------- stats */}
|
{/* ----------------------------------------------------------- stats */}
|
||||||
<Section className="pt-0 sm:pt-0">
|
<Section className="pt-0 sm:pt-0">
|
||||||
|
<Reveal>
|
||||||
<div className="rule-strong grid grid-cols-1 gap-x-10 gap-y-8 pt-8 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="rule-strong grid grid-cols-1 gap-x-10 gap-y-8 pt-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
{stats.map((s) => (
|
{stats.map((s) => (
|
||||||
<Stat key={s.label} {...s} />
|
<Stat key={s.label} {...s} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</Reveal>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* ----------------------------------------------------------- about */}
|
{/* ----------------------------------------------------------- about */}
|
||||||
|
|
@ -243,6 +245,7 @@ export default function EditorialSite() {
|
||||||
/>
|
/>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={60}>
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
<ul className="flex flex-col">
|
<ul className="flex flex-col">
|
||||||
{SERVICES.map((s) => (
|
{SERVICES.map((s) => (
|
||||||
|
|
@ -252,6 +255,7 @@ export default function EditorialSite() {
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</Reveal>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* -------------------------------------------------------- projects */}
|
{/* -------------------------------------------------------- projects */}
|
||||||
|
|
@ -265,6 +269,7 @@ export default function EditorialSite() {
|
||||||
/>
|
/>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={60}>
|
||||||
<ol className="mt-12 flex flex-col">
|
<ol className="mt-12 flex flex-col">
|
||||||
{codingProjects.map((p, i) => {
|
{codingProjects.map((p, i) => {
|
||||||
const { name, status } = splitStatus(p.title);
|
const { name, status } = splitStatus(p.title);
|
||||||
|
|
@ -289,6 +294,7 @@ export default function EditorialSite() {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ol>
|
</ol>
|
||||||
|
</Reveal>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* -------------------------------------------------------- hardware */}
|
{/* -------------------------------------------------------- hardware */}
|
||||||
|
|
@ -302,6 +308,7 @@ export default function EditorialSite() {
|
||||||
/>
|
/>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={60}>
|
||||||
<div className="mt-12 grid grid-cols-1 gap-x-16 gap-y-6 md:grid-cols-2">
|
<div className="mt-12 grid grid-cols-1 gap-x-16 gap-y-6 md:grid-cols-2">
|
||||||
{techProjects.map((t) => (
|
{techProjects.map((t) => (
|
||||||
<div key={t.title} className="rule-soft pt-4">
|
<div key={t.title} className="rule-soft pt-4">
|
||||||
|
|
@ -310,6 +317,7 @@ export default function EditorialSite() {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</Reveal>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* ----------------------------------------------------- contributions */}
|
{/* ----------------------------------------------------- contributions */}
|
||||||
|
|
@ -349,6 +357,7 @@ export default function EditorialSite() {
|
||||||
/>
|
/>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={60}>
|
||||||
<ul className="mt-12 flex flex-col">
|
<ul className="mt-12 flex flex-col">
|
||||||
{friends.map((f) => (
|
{friends.map((f) => (
|
||||||
<li key={f.name} className="rule">
|
<li key={f.name} className="rule">
|
||||||
|
|
@ -364,6 +373,7 @@ export default function EditorialSite() {
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
</Reveal>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* --------------------------------------------------------- lifting */}
|
{/* --------------------------------------------------------- lifting */}
|
||||||
|
|
@ -405,6 +415,7 @@ export default function EditorialSite() {
|
||||||
/>
|
/>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={60}>
|
||||||
<div className="rule-strong mt-12 grid grid-cols-2 gap-x-10 gap-y-3 pt-8 sm:grid-cols-3 lg:grid-cols-4">
|
<div className="rule-strong mt-12 grid grid-cols-2 gap-x-10 gap-y-3 pt-8 sm:grid-cols-3 lg:grid-cols-4">
|
||||||
{socials.map((s) => (
|
{socials.map((s) => (
|
||||||
<a
|
<a
|
||||||
|
|
@ -419,12 +430,14 @@ export default function EditorialSite() {
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</Reveal>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* ---------------------------------------------------------- footer */}
|
{/* ---------------------------------------------------------- footer */}
|
||||||
<footer className="shell pb-14 pt-10">
|
<footer className="shell pb-14 pt-10">
|
||||||
|
<Reveal>
|
||||||
<div className="rule-strong pt-8">
|
<div className="rule-strong pt-8">
|
||||||
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -447,6 +460,7 @@ export default function EditorialSite() {
|
||||||
<span>Follow the GPL-3.0 license (or don’t, I don’t mind).</span>
|
<span>Follow the GPL-3.0 license (or don’t, I don’t mind).</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Reveal>
|
||||||
</footer>
|
</footer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,10 @@ export function Reveal({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = ref.current;
|
const el = ref.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||||
|
el.classList.add("is-in");
|
||||||
// Only animate what starts below the fold. Anything already on screen stays
|
return;
|
||||||
// visible — otherwise the hero fades in after the section beneath it.
|
}
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const onScreen = rect.top < window.innerHeight && rect.bottom > 0;
|
|
||||||
if (onScreen) return;
|
|
||||||
|
|
||||||
// Hide it without a transition, then reveal when it scrolls into view.
|
|
||||||
el.style.transition = "none";
|
|
||||||
el.classList.add("reveal-armed");
|
|
||||||
void el.offsetHeight;
|
|
||||||
el.style.transition = "";
|
|
||||||
|
|
||||||
const io = new IntersectionObserver(
|
const io = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
|
|
@ -41,11 +32,22 @@ export function Reveal({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ rootMargin: "0px 0px -10% 0px", threshold: 0.05 }
|
{ rootMargin: "0px 0px -12% 0px", threshold: 0.08 }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const onScreen = rect.top < window.innerHeight && rect.bottom > 0;
|
||||||
|
if (onScreen) {
|
||||||
|
// First screenful: stagger in document order so it fills top to bottom.
|
||||||
|
const all = Array.from(document.querySelectorAll<HTMLElement>(".reveal"));
|
||||||
|
const order = Math.min(Math.max(0, all.indexOf(el)), 8);
|
||||||
|
el.style.setProperty("--delay", `${delay + order * 90}ms`);
|
||||||
|
requestAnimationFrame(() => el.classList.add("is-in"));
|
||||||
|
} else {
|
||||||
io.observe(el);
|
io.observe(el);
|
||||||
|
}
|
||||||
return () => io.disconnect();
|
return () => io.disconnect();
|
||||||
}, []);
|
}, [delay]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={`reveal ${className}`} style={{ "--delay": `${delay}ms` } as CSSProperties}>
|
<div ref={ref} className={`reveal ${className}`} style={{ "--delay": `${delay}ms` } as CSSProperties}>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@
|
||||||
|
|
||||||
--fill: rgba(235, 219, 178, 0.035);
|
--fill: rgba(235, 219, 178, 0.035);
|
||||||
--fill-strong: rgba(235, 219, 178, 0.07);
|
--fill-strong: rgba(235, 219, 178, 0.07);
|
||||||
|
|
||||||
|
--ease-cinematic: cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Beats page.css's :root when the editorial theme is active. */
|
/* Beats page.css's :root when the editorial theme is active. */
|
||||||
|
|
@ -230,19 +232,19 @@ html[data-device="phone"] .desktop-toggle {
|
||||||
Motion — scroll reveals only
|
Motion — scroll reveals only
|
||||||
------------------------------------------------------------------ */
|
------------------------------------------------------------------ */
|
||||||
|
|
||||||
/* Visible by default: Reveal only hides elements that start below the fold, so
|
/* Scroll reveal. Hidden until `.is-in` lands, and every block on the editorial
|
||||||
above-the-fold content (the hero) is never hidden while waiting for JS. */
|
page is wrapped, so the page fills in from top to bottom: the first screenful
|
||||||
|
cascades in document order on load, everything else reveals as it scrolls in.
|
||||||
|
A failed JS bundle leaves the page visible via the <noscript> rule in layout. */
|
||||||
.reveal {
|
.reveal {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate3d(0, 18px, 0);
|
||||||
transition:
|
transition:
|
||||||
opacity 0.55s ease-out,
|
opacity 0.8s var(--ease-cinematic),
|
||||||
transform 0.55s ease-out;
|
transform 0.8s var(--ease-cinematic);
|
||||||
transition-delay: var(--delay, 0ms);
|
transition-delay: var(--delay, 0ms);
|
||||||
}
|
}
|
||||||
.reveal.reveal-armed {
|
.reveal.is-in {
|
||||||
opacity: 0;
|
|
||||||
transform: translate3d(0, 14px, 0);
|
|
||||||
}
|
|
||||||
.reveal.reveal-armed.is-in {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,11 @@ export default function RootLayout({
|
||||||
className={`${news.variable} ${plex.variable} ${plexMono.variable}`}
|
className={`${news.variable} ${plex.variable} ${plexMono.variable}`}
|
||||||
>
|
>
|
||||||
<body className="antialiased">
|
<body className="antialiased">
|
||||||
|
<noscript>
|
||||||
|
{/* Scroll-reveal is progressive enhancement: without JS every block
|
||||||
|
must still be visible, since the base `.reveal` style is opacity 0. */}
|
||||||
|
<style>{`.reveal{opacity:1 !important;transform:none !important}`}</style>
|
||||||
|
</noscript>
|
||||||
<ThemeScript />
|
<ThemeScript />
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue