diff --git a/landing/src/pages/[...lang].astro b/landing/src/pages/[...lang].astro index 7d32c26..637ef95 100644 --- a/landing/src/pages/[...lang].astro +++ b/landing/src/pages/[...lang].astro @@ -25,14 +25,11 @@ const launcherName = launcherManifest.product; const registerPath = LANG_REGISTER_PATH[locale]; const accountPath = LANG_ACCOUNT_PATH[locale]; -// Per-OS download buttons (join step 2) from the synced manifest. Missing -// manifest → empty builds → no buttons (the build never fails on it). -const OS_LABELS: Record = { windows: "Windows", macos: "macOS", linux: "Linux" }; -const launcherBuilds = launcherManifest.builds.map((b) => ({ - label: b.label ?? `${OS_LABELS[b.os] ?? b.os}`, - hint: `${b.arch} · ${b.format}`, - url: b.url, -})); +// Per-OS download buttons (join step 2) are fetched client-side from the live +// launcher.json on page load (no build-time baking, no polling) — see the +// script below. We only pass the URL; buttons render into the #launcher-dl +// skeleton. URL derives from the distribution subdomain. +const launcherJsonUrl = `https://distribution.${site.baseDomain}/launcher/launcher.json`; // Build-time floating dust bits (index-derived, no runtime RNG). const dustBits = Array.from({ length: 16 }, (_, i) => { @@ -158,15 +155,11 @@ const dustBits = Array.from({ length: 16 }, (_, i) => { {t.join.s2.kicker}

{t.join.s2.h.replace("{name}", launcherName)}

{t.join.s2.p.replace(/\{name\}/g, launcherName)}

- {launcherBuilds.length > 0 && ( -
- {launcherBuilds.map((b) => ( - - {b.label} · {b.hint} - - ))} -
- )} +
+ + + +
@@ -323,6 +316,35 @@ const dustBits = Array.from({ length: 16 }, (_, i) => { ); els.forEach((el) => io.observe(el)); } + + // Launcher downloads — fetch the live launcher.json once on load (no + // polling) and render per-OS buttons into the #launcher-dl skeleton. + // launcher.json shape: { files: [{ name, os, arch, ext, url }] }. + const dl = document.getElementById("launcher-dl"); + const src = dl?.dataset.src; + if (dl && src) { + const OS: Record = { windows: "Windows", macos: "macOS", linux: "Linux" }; + fetch(src, { cache: "no-store" }) + .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))) + .then((data) => { + const files = Array.isArray(data?.files) ? data.files : []; + const frag = document.createDocumentFragment(); + for (const f of files) { + if (!f?.url) continue; + const a = document.createElement("a"); + a.className = "mc-btn sm"; + a.href = f.url; + a.append(`${OS[f.os] ?? f.os ?? "Download"} `); + const hint = document.createElement("span"); + hint.className = "hint"; + hint.textContent = `· ${[f.arch, f.ext].filter(Boolean).join(" · ")}`; + a.append(hint); + frag.append(a); + } + dl.replaceChildren(frag); // empty list → no buttons (skeleton cleared) + }) + .catch(() => dl.replaceChildren()); // unreachable/error → clear skeleton + } diff --git a/landing/src/styles/main.css b/landing/src/styles/main.css index 6892822..1099b90 100644 --- a/landing/src/styles/main.css +++ b/landing/src/styles/main.css @@ -207,6 +207,26 @@ section { position: relative; z-index: 1; } } .mc-btn.sm { font-size: 14px; padding: 10px 16px 12px; } +/* Launcher download skeleton (placeholder while launcher.json loads). */ +.dl-skeleton { + min-width: 9.5rem; + color: transparent; + pointer-events: none; + background-image: linear-gradient( + 90deg, + oklch(1 0 0 / 0.04), + oklch(1 0 0 / 0.14), + oklch(1 0 0 / 0.04) + ); + background-size: 200% 100%; + animation: dl-shimmer 1.2s linear infinite; +} +.dl-skeleton::after { content: "\00a0"; } /* keep height when text is empty */ +@keyframes dl-shimmer { + from { background-position: 200% 0; } + to { background-position: -200% 0; } +} + /* ---- Copy-IP chip ---- */ .ip-chip { display: inline-flex;