The website previously buried platform downloads and presented the desktop assistant, Agent Runtime, and broader work capabilities with competing priorities. It now leads with the desktop assistant and AI programming workbench positioning, surfaces cross-platform and domestic Linux compatibility, and uses a responsive interactive product preview with synchronized tilt and soft lighting. Release note: GoodBuddy 官网现在更清晰地展示桌面助手、Agent Runtime、跨平台与国产化支持,并提供更直观的动态产品预览。
175 lines
6.0 KiB
JavaScript
175 lines
6.0 KiB
JavaScript
(() => {
|
|
"use strict";
|
|
|
|
const root = document.documentElement;
|
|
const header = document.querySelector("[data-site-header]");
|
|
const menuToggle = document.querySelector("[data-menu-toggle]");
|
|
const navigation = document.querySelector("[data-navigation]");
|
|
const themeToggle = document.querySelector("[data-theme-toggle]");
|
|
const themeColor = document.querySelector('meta[name="theme-color"]');
|
|
const tiltStage = document.querySelector("[data-tilt-stage]");
|
|
const tiltCard = tiltStage?.querySelector("[data-tilt-card]");
|
|
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
const finePointer = window.matchMedia("(hover: hover) and (pointer: fine)");
|
|
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
|
|
const getSavedTheme = () => {
|
|
try {
|
|
const savedTheme = localStorage.getItem("goodbuddy-site-theme");
|
|
return savedTheme === "light" || savedTheme === "dark" ? savedTheme : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const applyTheme = (theme, persist = false) => {
|
|
root.dataset.theme = theme;
|
|
themeToggle?.setAttribute(
|
|
"aria-label",
|
|
theme === "dark" ? "切换为浅色主题" : "切换为深色主题",
|
|
);
|
|
themeColor?.setAttribute("content", theme === "dark" ? "#07101f" : "#f6f8fb");
|
|
|
|
if (persist) {
|
|
try {
|
|
localStorage.setItem("goodbuddy-site-theme", theme);
|
|
} catch {
|
|
// The selected theme still applies for the current page.
|
|
}
|
|
}
|
|
};
|
|
|
|
const closeMenu = () => {
|
|
header?.classList.remove("is-menu-open");
|
|
menuToggle?.setAttribute("aria-expanded", "false");
|
|
menuToggle?.setAttribute("aria-label", "打开导航");
|
|
};
|
|
|
|
const setHeaderState = () => {
|
|
header?.classList.toggle("is-scrolled", window.scrollY > 12);
|
|
};
|
|
|
|
applyTheme(getSavedTheme() ?? (systemTheme.matches ? "dark" : "light"));
|
|
setHeaderState();
|
|
|
|
themeToggle?.addEventListener("click", () => {
|
|
applyTheme(root.dataset.theme === "dark" ? "light" : "dark", true);
|
|
});
|
|
|
|
systemTheme.addEventListener("change", (event) => {
|
|
if (!getSavedTheme()) {
|
|
applyTheme(event.matches ? "dark" : "light");
|
|
}
|
|
});
|
|
|
|
menuToggle?.addEventListener("click", () => {
|
|
const willOpen = !header?.classList.contains("is-menu-open");
|
|
header?.classList.toggle("is-menu-open", willOpen);
|
|
menuToggle.setAttribute("aria-expanded", String(willOpen));
|
|
menuToggle.setAttribute("aria-label", willOpen ? "关闭导航" : "打开导航");
|
|
});
|
|
|
|
navigation?.addEventListener("click", (event) => {
|
|
if (event.target instanceof HTMLAnchorElement) {
|
|
closeMenu();
|
|
}
|
|
});
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape" && header?.classList.contains("is-menu-open")) {
|
|
closeMenu();
|
|
menuToggle?.focus();
|
|
}
|
|
});
|
|
|
|
document.addEventListener("click", (event) => {
|
|
if (
|
|
header?.classList.contains("is-menu-open") &&
|
|
event.target instanceof Node &&
|
|
!header.contains(event.target)
|
|
) {
|
|
closeMenu();
|
|
}
|
|
});
|
|
|
|
window.addEventListener("scroll", setHeaderState, { passive: true });
|
|
|
|
if (tiltStage instanceof HTMLElement && tiltCard instanceof HTMLElement) {
|
|
let tiltFrame = 0;
|
|
|
|
const resetTilt = () => {
|
|
window.cancelAnimationFrame(tiltFrame);
|
|
tiltStage.classList.remove("is-tilting");
|
|
tiltStage.style.setProperty("--spotlight-x", "50%");
|
|
tiltStage.style.setProperty("--spotlight-y", "50%");
|
|
tiltCard.style.setProperty("--spotlight-x", "50%");
|
|
tiltCard.style.setProperty("--spotlight-y", "50%");
|
|
tiltStage.style.setProperty("--scene-tilt-x", "0deg");
|
|
tiltStage.style.setProperty("--scene-tilt-y", "0deg");
|
|
};
|
|
|
|
const updateTilt = (event) => {
|
|
if (!finePointer.matches || reducedMotion.matches) {
|
|
resetTilt();
|
|
return;
|
|
}
|
|
|
|
const bounds = tiltStage.getBoundingClientRect();
|
|
const x = Math.min(Math.max((event.clientX - bounds.left) / bounds.width, 0), 1);
|
|
const y = Math.min(Math.max((event.clientY - bounds.top) / bounds.height, 0), 1);
|
|
|
|
window.cancelAnimationFrame(tiltFrame);
|
|
tiltFrame = window.requestAnimationFrame(() => {
|
|
const spotlightX = `${(x * 100).toFixed(1)}%`;
|
|
const spotlightY = `${(y * 100).toFixed(1)}%`;
|
|
tiltStage.classList.add("is-tilting");
|
|
tiltStage.style.setProperty("--spotlight-x", spotlightX);
|
|
tiltStage.style.setProperty("--spotlight-y", spotlightY);
|
|
tiltCard.style.setProperty("--spotlight-x", spotlightX);
|
|
tiltCard.style.setProperty("--spotlight-y", spotlightY);
|
|
tiltStage.style.setProperty("--scene-tilt-x", `${((0.5 - y) * 8).toFixed(2)}deg`);
|
|
tiltStage.style.setProperty("--scene-tilt-y", `${((x - 0.5) * 11).toFixed(2)}deg`);
|
|
});
|
|
};
|
|
|
|
tiltStage.addEventListener("pointermove", updateTilt, { passive: true });
|
|
tiltStage.addEventListener("pointerleave", resetTilt);
|
|
finePointer.addEventListener("change", resetTilt);
|
|
reducedMotion.addEventListener("change", resetTilt);
|
|
}
|
|
|
|
const sections = [...document.querySelectorAll("main section[id]")];
|
|
const navLinks = [...document.querySelectorAll('.site-navigation a[href^="#"]')];
|
|
|
|
if ("IntersectionObserver" in window) {
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
const visibleSection = entries
|
|
.filter((entry) => entry.isIntersecting)
|
|
.sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
|
|
|
|
if (!visibleSection) {
|
|
return;
|
|
}
|
|
|
|
navLinks.forEach((link) => {
|
|
const isCurrent = link.getAttribute("href") === `#${visibleSection.target.id}`;
|
|
if (isCurrent) {
|
|
link.setAttribute("aria-current", "true");
|
|
} else {
|
|
link.removeAttribute("aria-current");
|
|
}
|
|
});
|
|
},
|
|
{ rootMargin: "-25% 0px -55%", threshold: [0.05, 0.2, 0.5] },
|
|
);
|
|
|
|
sections.forEach((section) => observer.observe(section));
|
|
}
|
|
|
|
const currentYear = document.querySelector("[data-current-year]");
|
|
if (currentYear) {
|
|
currentYear.textContent = String(new Date().getFullYear());
|
|
}
|
|
})();
|