import { gsap } from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; import { ScrollSmoother } from "gsap/ScrollSmoother"; import { Download, Copy, Check } from "reicon"; const ICONS = { Download, Copy, Check }; const ICON_OPTS = { size: 18, strokeWidth: 1.5 }; for (const el of document.querySelectorAll(".i[data-icon]")) { const icon = ICONS[el.dataset.icon]; if (!icon) throw new Error(`unknown icon: ${el.dataset.icon}`); el.replaceChildren(icon(ICON_OPTS)); } /* Copy the convert command */ const copyBtn = document.getElementById("copy-btn"); const copyText = copyBtn.querySelector(".copy-text"); const copyIcon = copyBtn.querySelector(".i"); copyText.setAttribute("aria-live", "polite"); let copyTimer; let copySeq = 0; copyBtn.addEventListener("click", async () => { const cmd = document.getElementById("cmd-text").textContent.trim(); const seq = ++copySeq; let ok = true; try { await navigator.clipboard.writeText(cmd); } catch (err) { console.error("clipboard write failed", err); ok = false; } // Only the latest click may touch the feedback state if (seq !== copySeq) return; clearTimeout(copyTimer); copyBtn.classList.remove("done", "fail"); copyBtn.classList.add(ok ? "done" : "fail"); copyIcon.replaceChildren(ok ? Check(ICON_OPTS) : Copy(ICON_OPTS)); copyText.textContent = ok ? "已复制" : "复制失败"; copyTimer = setTimeout(() => { copyBtn.classList.remove("done", "fail"); copyIcon.replaceChildren(Copy(ICON_OPTS)); copyText.textContent = "复制"; }, 2000); }); /* Grid overlay: G key */ document.addEventListener("keydown", (e) => { if (e.key !== "g" && e.key !== "G") return; if (e.metaKey || e.ctrlKey || e.altKey) return; if (/^(input|textarea|select)$/i.test(e.target.tagName)) return; document.body.classList.toggle("grid-on"); }); /* Motion: smooth scrolling + reveals, skipped under reduced motion */ if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) { gsap.registerPlugin(ScrollTrigger, ScrollSmoother); ScrollSmoother.create({ wrapper: "#smooth-wrapper", content: "#smooth-content", smooth: 1, }); const enter = [".hero .eyebrow", ".hero h1 .line", ".hero .lede", ".hero .actions", ".hero .meta"]; gsap.set(enter, { y: 24, autoAlpha: 0 }); document.fonts.ready.then(() => { gsap.to(enter, { y: 0, autoAlpha: 1, duration: 0.7, ease: "power2.out", stagger: 0.09, }); }); // Document-tail elements never reach the 85% line, so they fire on entry const reveals = [ [".shot", "top 85%"], [".feat", "top 85%"], [".verify > *", "top 92%"], [".foot > *", "top bottom"], ]; for (const [sel, start] of reveals) { for (const el of gsap.utils.toArray(sel)) { gsap.from(el, { y: 24, autoAlpha: 0, duration: 0.6, ease: "power2.out", scrollTrigger: { trigger: el, start }, }); } } }