Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions apps/landing-page/app/_components/home-enhancer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
* - Headroom-style sticky header (hide on scroll down, reveal on up)
* - Live GitHub stars + latest release version injection
* - Live Wire contributors row (replaces the static fallback)
* - IntersectionObserver-driven `data-reveal` choreography (without
* this every section sits at `opacity: 0` and the page looks blank)
* - IntersectionObserver-driven `data-reveal` choreography, progressively
* enhanced so content stays visible when JavaScript is unavailable
*
* Inlining this script in only one of the entry points causes the
* "blank landing page" bug on localized homepages, so we centralize it
Expand Down Expand Up @@ -300,17 +300,25 @@
return;
}

const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
entry.target.dataset.revealed = 'true';
observer.unobserve(entry.target);
}
},
{ threshold: 0.12, rootMargin: '0px 0px -8% 0px' },
);
let observer;
try {
observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
entry.target.dataset.revealed = 'true';
observer.unobserve(entry.target);
}
},
{ threshold: 0.12, rootMargin: '0px 0px -8% 0px' },
);

for (const el of elements) observer.observe(el);
for (const el of elements) observer.observe(el);
document.documentElement.classList.add('reveal-ready');
} catch (error) {
observer?.disconnect();
document.documentElement.classList.remove('reveal-ready');
console.error('Reveal animation initialization failed', error);
}
})();
</script>
42 changes: 28 additions & 14 deletions apps/landing-page/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1906,7 +1906,8 @@ a.nav-mega-col-head.is-active {
* its `data-reveal`, so the IntersectionObserver in home-enhancer.astro flips it
* to `data-revealed='true'` on scroll-in and the cascade begins. The keyframes
* mirror the component's blur(10→5→0) + opacity(0→.5→1) + translateY steps. */
.hero h1.hero-title[data-reveal] {
.hero h1.hero-title[data-reveal],
.reveal-ready .hero h1.hero-title[data-reveal] {
/* Let the per-word blur be the whole motion: cancel the generic block reveal
translate/opacity so the frame itself doesn't slide or fade. */
opacity: 1;
Expand All @@ -1916,12 +1917,17 @@ a.nav-mega-col-head.is-active {
}
.blur-word {
display: inline-block;
opacity: 1;
filter: none;
transform: none;
}
.reveal-ready .blur-word {
opacity: 0;
filter: blur(10px);
transform: translateY(-50px);
will-change: transform, filter, opacity;
}
.hero-title[data-revealed='true'] .blur-word {
.reveal-ready .hero-title[data-revealed='true'] .blur-word {
animation: blurTextIn 0.38s cubic-bezier(0.23, 1, 0.32, 1) both;
animation-delay: calc(var(--i, 0) * 0.035s);
}
Expand Down Expand Up @@ -5113,7 +5119,7 @@ section.tight { padding: 90px 0; }
pointer-events: none;
}
/* The product window rises up from below when the CTA scrolls into view. */
.cta-window[data-reveal] {
.reveal-ready .cta-window[data-reveal]:not([data-revealed='true']) {
translate: 0 96px;
}
.cta-dance-inner {
Expand Down Expand Up @@ -5834,28 +5840,35 @@ footer .container {

/* ---------- scroll-reveal motion ----------
*
* Driven by `app/_components/reveal-root.tsx`. Elements with
* `data-reveal` start hidden + offset; the observer sets
* `data-revealed='true'` once they enter the viewport, triggering
* the transition.
* Content is visible by default. Once the homepage observer is successfully
* bound, it adds `.reveal-ready` to opt unrevealed elements into the hidden
* start state. The observer then sets `data-revealed='true'` on viewport entry.
* If JavaScript is disabled or reveal setup fails, the class is never added and
* the complete page remains visible.
*
* Uses `translate` / `scale` longhand properties (not `transform`) so
* that elements like `.work-card` keep their static `transform: rotate()`
* intact while still translating in.
*/
[data-reveal] {
Comment thread
This conversation was marked as resolved.
opacity: 0;
translate: 0 28px;
opacity: 1;
translate: 0 0;
scale: 1;
transition:
opacity 900ms cubic-bezier(0.22, 1, 0.36, 1) var(--reveal-delay, 0ms),
translate 900ms cubic-bezier(0.22, 1, 0.36, 1) var(--reveal-delay, 0ms),
scale 900ms cubic-bezier(0.22, 1, 0.36, 1) var(--reveal-delay, 0ms);
will-change: auto;
}
.reveal-ready [data-reveal]:not([data-revealed='true']) {
Comment thread
This conversation was marked as resolved.
opacity: 0;
translate: 0 28px;
will-change: opacity, translate, scale;
}
[data-reveal='left'] { translate: -36px 0; }
[data-reveal='right'] { translate: 36px 0; }
[data-reveal='scale'] { translate: 0 0; scale: 0.96; }
[data-reveal='rise-lg'] { translate: 0 64px; scale: 0.985; }
.reveal-ready [data-reveal='left']:not([data-revealed='true']) { translate: -36px 0; }
.reveal-ready [data-reveal='right']:not([data-revealed='true']) { translate: 36px 0; }
.reveal-ready [data-reveal='scale']:not([data-revealed='true']) { translate: 0 0; scale: 0.96; }
.reveal-ready [data-reveal='rise-lg']:not([data-revealed='true']) { translate: 0 64px; scale: 0.985; }
[data-reveal][data-revealed='true'] {
opacity: 1;
translate: 0 0;
Expand Down Expand Up @@ -6202,7 +6215,8 @@ footer .container {
border-radius: 10px;
}
/* cancel the translate-up reveal so the static image isn't shifted */
.cta-window[data-reveal] { translate: 0 0; }
.cta-window[data-reveal],
.reveal-ready .cta-window[data-reveal]:not([data-revealed='true']) { translate: 0 0; }
/* Copy block (title → tag → buttons) leads the stack; the window follows via
its base order:1 (inner stays at the default order:0). */
.cta-dance-inner { padding: 0; align-items: center; }
Expand Down
30 changes: 19 additions & 11 deletions apps/landing-page/app/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -1809,18 +1809,26 @@ const cobeEnhancerUrl = `/enhancers/cobe.js?v=${
return;
}

const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
entry.target.dataset.revealed = 'true';
observer.unobserve(entry.target);
}
},
{ threshold: 0.12, rootMargin: '0px 0px -8% 0px' },
);
let observer;
try {
observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
entry.target.dataset.revealed = 'true';
observer.unobserve(entry.target);
}
},
{ threshold: 0.12, rootMargin: '0px 0px -8% 0px' },
);

for (const el of elements) observer.observe(el);
for (const el of elements) observer.observe(el);
document.documentElement.classList.add('reveal-ready');
} catch (error) {
observer?.disconnect();
document.documentElement.classList.remove('reveal-ready');
console.error('Reveal animation initialization failed', error);
}
})();
</script>
<script is:inline>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { test } from 'node:test';

const stylesSource = readFileSync(new URL('../app/globals.css', import.meta.url), 'utf8');
const homepageSource = readFileSync(new URL('../app/pages/index.astro', import.meta.url), 'utf8');
const sharedEnhancerSource = readFileSync(
new URL('../app/_components/home-enhancer.astro', import.meta.url),
'utf8',
);

test('keeps reveal content visible until the animation observer is ready', () => {
assert.match(
stylesSource,
/\[data-reveal\]\s*\{[\s\S]*?opacity:\s*1;[\s\S]*?translate:\s*0 0;[\s\S]*?\}/,
);
assert.match(
stylesSource,
/\.reveal-ready \[data-reveal\]:not\(\[data-revealed='true'\]\)\s*\{[\s\S]*?opacity:\s*0;/,
);
assert.match(
stylesSource,
/\.blur-word\s*\{[\s\S]*?opacity:\s*1;[\s\S]*?filter:\s*none;[\s\S]*?transform:\s*none;/,
);
assert.match(stylesSource, /\.reveal-ready \.blur-word\s*\{[\s\S]*?opacity:\s*0;/);
});

test('preserves the hero and mobile CTA reveal exceptions after readiness', () => {
assert.match(
stylesSource,
/\.reveal-ready \.hero h1\.hero-title\[data-reveal\]\s*\{[\s\S]*?opacity:\s*1;[\s\S]*?translate:\s*0 0;/,
);
assert.match(
stylesSource,
/@media \(max-width:\s*720px\)[\s\S]*?\.reveal-ready \.cta-window\[data-reveal\]:not\(\[data-revealed='true'\]\)\s*\{\s*translate:\s*0 0;/,
);
});

for (const [name, source] of [
['canonical homepage', homepageSource],
['shared homepage enhancer', sharedEnhancerSource],
] as const) {
test(`${name} enables reveal styles only after observer binding succeeds`, () => {
const observeIndex = source.indexOf('observer.observe(el)');
const readyIndex = source.indexOf("classList.add('reveal-ready')");

assert.notEqual(observeIndex, -1, `${name} does not bind reveal elements`);
assert.ok(readyIndex > observeIndex, `${name} enables hidden states before observer binding`);
assert.match(source, /catch \(error\)[\s\S]*?classList\.remove\('reveal-ready'\)/);
});
}
53 changes: 33 additions & 20 deletions design-templates/open-design-landing/example.html
Original file line number Diff line number Diff line change
Expand Up @@ -1908,28 +1908,33 @@

/* ---------- scroll-reveal motion ----------
*
* Driven by `app/_components/reveal-root.tsx`. Elements with
* `data-reveal` start hidden + offset; the observer sets
* `data-revealed='true'` once they enter the viewport, triggering
* the transition.
* Content is visible by default. Once the observer is successfully bound,
* `.reveal-ready` opts unrevealed elements into the hidden start state. If
* JavaScript is disabled or setup fails, the complete page remains visible.
*
* Uses `translate` / `scale` longhand properties (not `transform`) so
* that elements like `.work-card` keep their static `transform: rotate()`
* intact while still translating in.
*/
[data-reveal] {
opacity: 0;
translate: 0 28px;
opacity: 1;
translate: 0 0;
scale: 1;
transition:
opacity 900ms cubic-bezier(0.22, 1, 0.36, 1) var(--reveal-delay, 0ms),
translate 900ms cubic-bezier(0.22, 1, 0.36, 1) var(--reveal-delay, 0ms),
scale 900ms cubic-bezier(0.22, 1, 0.36, 1) var(--reveal-delay, 0ms);
will-change: auto;
}
.reveal-ready [data-reveal]:not([data-revealed='true']) {
Comment thread
This conversation was marked as resolved.
opacity: 0;
translate: 0 28px;
will-change: opacity, translate, scale;
}
[data-reveal='left'] { translate: -36px 0; }
[data-reveal='right'] { translate: 36px 0; }
[data-reveal='scale'] { translate: 0 0; scale: 0.96; }
[data-reveal='rise-lg'] { translate: 0 64px; scale: 0.985; }
.reveal-ready [data-reveal='left']:not([data-revealed='true']) { translate: -36px 0; }
.reveal-ready [data-reveal='right']:not([data-revealed='true']) { translate: 36px 0; }
.reveal-ready [data-reveal='scale']:not([data-revealed='true']) { translate: 0 0; scale: 0.96; }
.reveal-ready [data-reveal='rise-lg']:not([data-revealed='true']) { translate: 0 64px; scale: 0.985; }
[data-reveal][data-revealed='true'] {
opacity: 1;
translate: 0 0;
Expand Down Expand Up @@ -2942,25 +2947,33 @@ <h5>Docs</h5>

<script>
/*
* Scroll-reveal observer — mirrors apps/landing-page/app/_components/reveal-root.tsx.
* Scroll-reveal observer — mirrors the landing-page homepage enhancer.
* Watches every [data-reveal] element and flips data-revealed='true'
* when it first enters the viewport, triggering the CSS transition.
*/
(function () {
var elements = document.querySelectorAll('[data-reveal]:not([data-revealed])');
if (!elements.length) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches || !('IntersectionObserver' in window)) {
for (var i = 0; i < elements.length; i++) elements[i].dataset.revealed = 'true';
return;
}
var observer = new IntersectionObserver(function (entries) {
for (var i = 0; i < entries.length; i++) {
if (!entries[i].isIntersecting) continue;
entries[i].target.dataset.revealed = 'true';
observer.unobserve(entries[i].target);
}
}, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
for (var j = 0; j < elements.length; j++) observer.observe(elements[j]);
var observer;
try {
observer = new IntersectionObserver(function (entries) {
for (var i = 0; i < entries.length; i++) {
if (!entries[i].isIntersecting) continue;
entries[i].target.dataset.revealed = 'true';
observer.unobserve(entries[i].target);
}
}, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
for (var j = 0; j < elements.length; j++) observer.observe(elements[j]);
document.documentElement.classList.add('reveal-ready');
} catch (error) {
if (observer) observer.disconnect();
document.documentElement.classList.remove('reveal-ready');
console.error('Reveal animation initialization failed', error);
}
})();

/*
Expand Down
28 changes: 18 additions & 10 deletions design-templates/open-design-landing/scripts/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -673,25 +673,33 @@ function renderWire(i: EditorialCollageInputs): string {
const REVEAL_AND_NAV_SCRIPT = `
<script>
/*
* Scroll-reveal observer — mirrors apps/landing-page/app/_components/reveal-root.tsx.
* Scroll-reveal observer — mirrors the landing-page homepage enhancer.
* Watches every [data-reveal] element and flips data-revealed='true'
* when it first enters the viewport, triggering the CSS transition.
*/
(function () {
var elements = document.querySelectorAll('[data-reveal]:not([data-revealed])');
if (!elements.length) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches || !('IntersectionObserver' in window)) {
for (var i = 0; i < elements.length; i++) elements[i].dataset.revealed = 'true';
return;
}
var observer = new IntersectionObserver(function (entries) {
for (var i = 0; i < entries.length; i++) {
if (!entries[i].isIntersecting) continue;
entries[i].target.dataset.revealed = 'true';
observer.unobserve(entries[i].target);
}
}, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
for (var j = 0; j < elements.length; j++) observer.observe(elements[j]);
var observer;
try {
observer = new IntersectionObserver(function (entries) {
for (var i = 0; i < entries.length; i++) {
if (!entries[i].isIntersecting) continue;
entries[i].target.dataset.revealed = 'true';
observer.unobserve(entries[i].target);
}
}, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
for (var j = 0; j < elements.length; j++) observer.observe(elements[j]);
document.documentElement.classList.add('reveal-ready');
} catch (error) {
if (observer) observer.disconnect();
document.documentElement.classList.remove('reveal-ready');
console.error('Reveal animation initialization failed', error);
}
})();

/*
Expand Down
Loading
Loading