feat(www): landing page (#25)
* design: landing page spec + app icon asset * chore(www): add anime.js, fonts, and landing page screenshot assets * feat(www): add design tokens, fonts, shared layout * feat(www): add landing page sections and styling * feat(www): add theme toggle and anime.js viewport animations * feat(www): feedback updates — no hero card, os-aware download, footer email, lowercase colored heading * feat(www): remove screenshot borders, move macos hint below ctas, mobile github fallback * feat(www): keep hero CTAs side by side * feat(www): set initial nav border to transparent * feat(www): add Rybbit prod-only, sitemap, SEO head, OG image, robots.txt, 404 page * docs(AGENTS): include www landing page version in release sync checklist * chore(www): add railpack.json for Dokploy deploy (force node provider, TZ) * chore(www): use bun for Railpack deploy (packageManager + standalone www/bun.lock) * docs(AGENTS): note www/bun.lock sync for Railpack deploys * chore(www): add vitest test tooling * feat(www): add FAQ data source * feat(www): add SEO JSON-LD builders * feat(www): add Seo JSON-LD component * feat(www): wire Seo component and social/meta tags into Layout * feat(www): add homepage JSON-LD, keyword H1, data-driven FAQ * fix(www): point Roadmap footer link to GitHub * feat(www): add lastmod to sitemap * perf(www): self-host anime.js * refactor(www): move nav and footer into shared Layout * feat(www): add changelog page * fix(www): make shared component styles global in Layout * perf(www): optimize screenshots with Astro Image (webp/avif + srcset) * feat(www): add web app manifest * feat(www): add helpful links to 404 page * fix(www): shorten homepage H1 copy * fix(www): OS-aware navbar download + footer layout * feat(www): build full changelog from v0.7.0 to v0.7.10
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
|
||||
<g transform="matrix(1.464129,0,0,1.464129,-67.349927,-273.792094)">
|
||||
<path d="M729,357.75L729,699.25C729,793.489 652.489,870 558.25,870L216.75,870C122.511,870 46,793.489 46,699.25L46,357.75C46,263.511 122.511,187 216.75,187L558.25,187C652.489,187 729,263.511 729,357.75Z" style="fill:url(#_Linear1);stroke:black;stroke-width:0.68px;"/>
|
||||
</g>
|
||||
<g id="Icon">
|
||||
<g transform="matrix(1.186992,0,0,1.186992,-93.495935,-96.300813)">
|
||||
<path d="M325,351C325,305.743 403.415,269 500,269C596.585,269 675,305.743 675,351C675,365.931 666.465,379.936 651.557,392C640.124,401.252 624.943,409.364 606.99,415.876C577.4,426.609 540.28,433 500,433C459.711,433 422.583,426.607 392.99,415.869C375.046,409.358 359.872,401.249 348.443,392C333.535,379.936 325,365.931 325,351Z" style="fill:white;stroke:black;stroke-width:14.04px;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.186992,0,0,1.186992,-93.495935,-96.300813)">
|
||||
<path d="M325,433L325,351C325,365.931 333.535,379.936 348.443,392C359.872,401.249 375.046,409.358 392.99,415.869L392.99,497.869C375.046,491.358 359.872,483.249 348.443,474C333.535,461.936 325,447.931 325,433Z" style="fill:white;stroke:black;stroke-width:14.04px;"/>
|
||||
</g>
|
||||
<path d="M626.996,592.007C591.874,604.748 547.812,612.333 500,612.333L500,515C547.812,515 591.874,507.414 626.996,494.674C648.307,486.944 666.326,477.316 679.897,466.333C697.592,452.013 707.724,435.39 707.724,417.667L707.724,515C707.724,532.723 697.592,549.347 679.897,563.667C666.326,574.649 648.307,584.277 626.996,592.007Z" style="fill:white;stroke:black;stroke-width:16.67px;"/>
|
||||
<g transform="matrix(1.186992,0,0,1.186992,-93.495935,-96.300813)">
|
||||
<path d="M325,515L325,433C325,447.931 333.535,461.936 348.443,474C359.872,483.249 375.046,491.358 392.99,497.869L392.99,579.869C375.046,573.358 359.872,565.249 348.443,556C333.535,543.936 325,529.931 325,515Z" style="fill:white;stroke:black;stroke-width:14.04px;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.186992,0,0,1.186992,-93.495935,-96.300813)">
|
||||
<path d="M325,597L325,515C325,529.931 333.535,543.936 348.443,556C359.872,565.249 375.046,573.358 392.99,579.869L392.99,661.869C375.046,655.358 359.872,647.249 348.443,638C333.535,625.936 325,611.931 325,597Z" style="fill:white;stroke:black;stroke-width:14.04px;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.186992,0,0,1.186992,-93.495935,-96.300813)">
|
||||
<path d="M675,515L675,597C675,611.931 666.465,625.936 651.557,638C640.124,647.252 624.943,655.364 606.99,661.876L606.99,579.876C624.943,573.364 640.124,565.252 651.557,556C666.465,543.936 675,529.931 675,515Z" style="fill:white;stroke:black;stroke-width:14.04px;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.186992,0,0,1.186992,-93.495935,-96.300813)">
|
||||
<path d="M675,597L675,679C675,705.383 648.352,728.872 606.99,743.876L606.99,661.876C624.943,655.364 640.124,647.252 651.557,638C666.465,625.936 675,611.931 675,597Z" style="fill:white;stroke:black;stroke-width:14.04px;"/>
|
||||
</g>
|
||||
<path d="M372.98,786.665C323.898,768.856 292.276,740.978 292.276,709.667L292.276,612.333C292.276,630.057 302.408,646.68 320.103,661C333.669,671.979 351.68,681.603 372.98,689.332C408.107,702.078 452.177,709.667 500,709.667C547.812,709.667 591.874,702.081 626.996,689.341L626.996,786.674C591.874,799.414 547.812,807 500,807C452.177,807 408.107,799.411 372.98,786.665Z" style="fill:white;stroke:black;stroke-width:16.67px;"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(683,683,-683,683,46,187)"><stop offset="0" style="stop-color:rgb(108,108,108);stop-opacity:1"/><stop offset="0.17" style="stop-color:rgb(15,15,15);stop-opacity:1"/><stop offset="0.5" style="stop-color:black;stop-opacity:1"/><stop offset="0.83" style="stop-color:rgb(15,15,15);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(108,108,108);stop-opacity:1"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,215 @@
|
||||
(function () {
|
||||
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
// Theme handling
|
||||
const html = document.documentElement;
|
||||
const stored = localStorage.getItem("gridline-theme");
|
||||
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
|
||||
function applyTheme(theme) {
|
||||
if (theme === "light") {
|
||||
html.classList.add("light");
|
||||
html.removeAttribute("data-theme-auto");
|
||||
} else if (theme === "dark") {
|
||||
html.classList.remove("light");
|
||||
html.removeAttribute("data-theme-auto");
|
||||
} else {
|
||||
// auto
|
||||
html.classList.toggle("light", !systemDark && window.matchMedia("(prefers-color-scheme: light)").matches);
|
||||
html.setAttribute("data-theme-auto", "");
|
||||
}
|
||||
updateToggleIcon();
|
||||
}
|
||||
|
||||
function updateToggleIcon() {
|
||||
const suns = document.querySelectorAll(".theme-icon-sun");
|
||||
const moons = document.querySelectorAll(".theme-icon-moon");
|
||||
const isLight = html.classList.contains("light");
|
||||
suns.forEach((el) => el.classList.toggle("hidden", !isLight));
|
||||
moons.forEach((el) => el.classList.toggle("hidden", isLight));
|
||||
}
|
||||
|
||||
applyTheme(stored);
|
||||
|
||||
document.getElementById("theme-toggle")?.addEventListener("click", () => {
|
||||
const isLight = html.classList.contains("light");
|
||||
const next = isLight ? "dark" : "light";
|
||||
localStorage.setItem("gridline-theme", next);
|
||||
applyTheme(next);
|
||||
});
|
||||
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", () => {
|
||||
if (html.hasAttribute("data-theme-auto")) {
|
||||
applyTheme("auto");
|
||||
}
|
||||
});
|
||||
|
||||
// OS-aware download buttons (nav + hero). Any element with [data-download]
|
||||
// gets the platform-specific download URL; labels are only rewritten when the
|
||||
// element does NOT opt out via a data-download-label attribute (nav keeps its
|
||||
// compact "Download" label, hero gets "Download for macOS" etc.).
|
||||
document.querySelectorAll("a[data-download]").forEach((downloadBtn) => {
|
||||
const platform = navigator.platform || "";
|
||||
const userAgent = navigator.userAgent || "";
|
||||
const isMac = /Mac/i.test(platform) && !/iPhone|iPad/i.test(userAgent);
|
||||
const isWin = /Win/i.test(platform);
|
||||
const isLinux = /Linux/i.test(platform) && !/Android/i.test(userAgent);
|
||||
const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(userAgent);
|
||||
const keepLabel = downloadBtn.hasAttribute("data-download-label");
|
||||
|
||||
const releasesUrl = "https://github.com/AdrianBonpin/gridline/releases";
|
||||
const version = "0.7.10";
|
||||
|
||||
if (isMobile) {
|
||||
downloadBtn.href = releasesUrl;
|
||||
if (!keepLabel) downloadBtn.textContent = "Get it on GitHub";
|
||||
} else if (isMac) {
|
||||
downloadBtn.href = `${releasesUrl}/download/v${version}/Gridline_${version}_aarch64.dmg`;
|
||||
if (!keepLabel) downloadBtn.textContent = "Download for macOS";
|
||||
if (downloadBtn.id === "download") {
|
||||
document.getElementById("macos-hint")?.classList.remove("hidden");
|
||||
}
|
||||
} else if (isWin) {
|
||||
downloadBtn.href = `${releasesUrl}/download/v${version}/Gridline_${version}_x64-setup.exe`;
|
||||
if (!keepLabel) downloadBtn.textContent = "Download for Windows";
|
||||
} else if (isLinux) {
|
||||
downloadBtn.href = `${releasesUrl}/download/v${version}/Gridline-${version}-1.x86_64.rpm`;
|
||||
if (!keepLabel) downloadBtn.textContent = "Download for Linux";
|
||||
} else {
|
||||
downloadBtn.href = releasesUrl;
|
||||
if (!keepLabel) downloadBtn.textContent = "Download";
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("copy-quarantine")?.addEventListener("click", async () => {
|
||||
const command = "sudo xattr -dr com.apple.quarantine /Applications/Gridline.app";
|
||||
try {
|
||||
await navigator.clipboard.writeText(command);
|
||||
const btn = document.getElementById("copy-quarantine");
|
||||
if (!btn) return;
|
||||
const original = btn.textContent;
|
||||
btn.textContent = "Copied";
|
||||
setTimeout(() => (btn.textContent = original), 1500);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
// Nav scroll background
|
||||
const nav = document.getElementById("nav");
|
||||
function onScroll() {
|
||||
if (!nav) return;
|
||||
if (window.scrollY > 40) {
|
||||
nav.classList.add("bg-[var(--surface)]", "backdrop-blur-md", "border-b", "border-[var(--border)]");
|
||||
} else {
|
||||
nav.classList.remove("bg-[var(--surface)]", "backdrop-blur-md", "border-b", "border-[var(--border)]");
|
||||
}
|
||||
}
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
onScroll();
|
||||
|
||||
// Anime.js animations
|
||||
let anime;
|
||||
try {
|
||||
anime = require ? null : window.anime;
|
||||
} catch {}
|
||||
|
||||
function loadAnime(callback) {
|
||||
if (window.anime) {
|
||||
callback(window.anime);
|
||||
return;
|
||||
}
|
||||
const script = document.createElement("script");
|
||||
script.src = "/vendor/anime.min.js";
|
||||
script.onload = () => callback(window.anime);
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
loadAnime((anime) => {
|
||||
// Hero entrance
|
||||
const hero = document.getElementById("hero");
|
||||
if (hero) {
|
||||
const eyebrow = hero.querySelector(".hero-animate > .font-mono");
|
||||
const h1 = hero.querySelector("h1");
|
||||
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 heroTl = anime.timeline({ easing: "easeOutExpo" });
|
||||
|
||||
if (!reducedMotion) {
|
||||
anime.set([eyebrow, h1, subtitle, buttons, screenshot], { 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 (screenshot) heroTl.add({ targets: screenshot, opacity: [0, 1], scale: [0.96, 1], duration: 900 }, 450);
|
||||
}
|
||||
}
|
||||
|
||||
// Section scroll reveals
|
||||
const sections = document.querySelectorAll(".section-animate");
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
const children = entry.target.children;
|
||||
if (reducedMotion) {
|
||||
anime({ targets: children, opacity: [0, 1], duration: 400, easing: "linear" });
|
||||
} else {
|
||||
anime({
|
||||
targets: children,
|
||||
opacity: [0, 1],
|
||||
translateY: [40, 0],
|
||||
duration: 700,
|
||||
delay: anime.stagger(100),
|
||||
easing: "easeOutCubic",
|
||||
});
|
||||
}
|
||||
observer.unobserve(entry.target);
|
||||
});
|
||||
},
|
||||
{ threshold: 0.15 }
|
||||
);
|
||||
sections.forEach((section) => observer.observe(section));
|
||||
});
|
||||
|
||||
// FAQ smooth height animation via native details toggle + anime fallback
|
||||
document.querySelectorAll(".faq-item").forEach((item) => {
|
||||
const summary = item.querySelector(".faq-summary");
|
||||
const body = item.querySelector(".faq-body");
|
||||
if (!summary || !body) return;
|
||||
|
||||
summary.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const isOpen = item.hasAttribute("open");
|
||||
if (isOpen) {
|
||||
if (window.anime && !reducedMotion) {
|
||||
window.anime({
|
||||
targets: body,
|
||||
height: [body.scrollHeight, 0],
|
||||
opacity: [1, 0],
|
||||
duration: 250,
|
||||
easing: "easeInQuad",
|
||||
complete: () => item.removeAttribute("open"),
|
||||
});
|
||||
} else {
|
||||
item.removeAttribute("open");
|
||||
}
|
||||
} else {
|
||||
item.setAttribute("open", "");
|
||||
if (window.anime && !reducedMotion) {
|
||||
window.anime({
|
||||
targets: body,
|
||||
height: [0, body.scrollHeight],
|
||||
opacity: [0, 1],
|
||||
duration: 300,
|
||||
easing: "easeOutQuad",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 229 KiB |
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Gridline",
|
||||
"short_name": "Gridline",
|
||||
"description": "The open-source database GUI for PostgreSQL, MySQL, SQLite, and Redis.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0a0a0a",
|
||||
"theme_color": "#0a0a0a",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/gridline-icon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+8
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user