fix(www): prevent flicker and reveal macos-hint on landing page (#27)

Hide entry-animated elements at first paint (html.anime-ready guard in
global.css) so the landing page no longer flashes visible content before
anime.js loads. Adds a <noscript> fallback and a JS load-error fallback to
guarantee content is never permanently hidden, and reveals the hero
immediately for prefers-reduced-motion users. Includes #macos-hint in the
hero entrance timeline so the macOS command box animates in correctly.
This commit is contained in:
2026-08-17 10:30:04 +08:00
committed by GitHub
parent 73c8cdd79b
commit c22e675f2b
3 changed files with 47 additions and 4 deletions
+17 -3
View File
@@ -122,9 +122,16 @@
const script = document.createElement("script");
script.src = "/vendor/anime.min.js";
script.onload = () => callback(window.anime);
script.onerror = () => revealAnimatedContent();
document.head.appendChild(script);
}
// Revert the entry-animation hiding (html.anime-ready CSS) so content is
// always visible even if the animation library never loads.
function revealAnimatedContent() {
document.documentElement.classList.remove("anime-ready");
}
loadAnime((anime) => {
// Hero entrance
const hero = document.getElementById("hero");
@@ -134,17 +141,24 @@
const subtitle = hero.querySelector(".hero-animate > p:nth-of-type(2)");
const buttons = hero.querySelector(".hero-animate > div");
const screenshot = hero.querySelector(".hero-screenshot");
const macosHint = hero.querySelector("#macos-hint");
const heroTl = anime.timeline({ easing: "easeOutExpo" });
if (reducedMotion) {
// Reduced motion: reveal the hero immediately (CSS hides it at first
// paint to avoid flicker, but the timeline below is skipped). Section
// reveals below still run through the IntersectionObserver.
anime.set([eyebrow, h1, subtitle, buttons, screenshot, macosHint], { opacity: 1, translateY: 0 });
} else {
const heroTl = anime.timeline({ easing: "easeOutExpo" });
if (!reducedMotion) {
anime.set([eyebrow, h1, subtitle, buttons, screenshot], { opacity: 0, translateY: 20 });
anime.set([eyebrow, h1, subtitle, buttons, screenshot, macosHint], { opacity: 0, translateY: 20 });
anime.set(screenshot, { opacity: 0, scale: 0.96, translateY: 0 });
if (eyebrow) heroTl.add({ targets: eyebrow, opacity: [0, 1], translateY: [15, 0] }, 0);
if (h1) heroTl.add({ targets: h1, opacity: [0, 1], translateY: [30, 0], duration: 800 }, 100);
if (subtitle) heroTl.add({ targets: subtitle, opacity: [0, 1], translateY: [20, 0] }, 250);
if (buttons) heroTl.add({ targets: buttons, opacity: [0, 1], translateY: [20, 0] }, 350);
if (macosHint) heroTl.add({ targets: macosHint, opacity: [0, 1], translateY: [20, 0] }, 400);
if (screenshot) heroTl.add({ targets: screenshot, opacity: [0, 1], scale: [0.96, 1], duration: 900 }, 450);
}
}