---import Layout from "../layouts/Layout.astro";import { AnimatedBeamDemo } from "../components/animated-beam-demo";import { SelfHostContactModal } from "../components/self-host-contact-modal";--- <Layout> <main class="bg-soft min-h-dvh relative antialiased"> {/* Pattern + fade — span the top 100vh, behind everything */} <div class="pattern-bg pattern-graph" style="position: absolute; top: 0; left: 0; right: 0; height: 100vh; z-index: 1; pointer-events: none;" > </div> <div style="position: absolute; top: 0; left: 0; right: 0; height: 100vh; z-index: 2; pointer-events: none; background: linear-gradient(180deg, transparent 0%, transparent 65%, var(--color-surface) 100%);" > </div> <div class="relative z-10"> {/* NAV */} <nav> <div class="max-w-[1200px] mx-auto px-6 sm:px-10 h-14 flex items-center justify-between" > <a href="/" class="flex items-center gap-2.5"> <img src="/favicon-192.png" alt="" class="w-5 h-5" /> <span class="text-[15px] font-semibold tracking-[-0.01em] text-ink" >executor</span > </a> <div class="flex items-center gap-5"> <a href="https://github.com/RhysSullivan/executor" class="text-[14px] font-medium text-ink-2 hover:text-ink transition-colors" >GitHub</a > <a href="/cloud" class="btn-primary">Try Cloud →</a> </div> </div> </nav> {/* HERO — centered, tight */} <section class="relative"> <div class="relative z-10 px-6 sm:px-10 pt-28 pb-32 sm:pt-36 sm:pb-44"> <div class="max-w-[920px] mx-auto text-center"> <h1 class="rise-1 text-[clamp(2.8rem,7.2vw,6rem)] font-semibold tracking-[-0.025em] leading-[1.02] text-ink mb-12 mx-auto max-w-[18ch] text-balance" > Connect any agent to <span class="serif-italic">everything</span>. </h1> <div class="rise-2 max-w-[760px] mx-auto mb-12"> <div class="surface-card p-6 sm:p-7"> <AnimatedBeamDemo client:load variant="stripe" /> </div> </div> <p class="rise-3 text-[18px] leading-[1.55] text-ink-2 mx-auto max-w-[44ch] mb-9 text-pretty" > One gateway. Every tool. Every team. </p> {/* ── Toggle: Try in Cloud / Try locally ── */} <div class="rise-4 hero-toggle flex flex-col items-center gap-7" data-active="cloud" > <div class="tab-toggle" data-active="cloud" role="tablist" aria-label="Choose how to try Executor" > <span class="tab-indicator" aria-hidden="true"></span> <button type="button" role="tab" aria-selected="true" data-tab="cloud" > Cloud </button> <button type="button" role="tab" aria-selected="false" data-tab="local" > Local </button> </div> {/* Cloud pane */} <div class="pane flex-col items-center gap-4" data-pane="cloud" role="tabpanel" aria-labelledby="tab-cloud" > <p class="pane-desc"> Built for teams. Plug into any agent via MCP. </p> <a href="/cloud" class="btn-primary btn-primary--hero" >Try Cloud →</a > </div> {/* Local pane — auto-detected download */} <div class="pane flex-col items-center gap-4" data-pane="local" role="tabpanel" aria-labelledby="tab-local" data-download > <p class="pane-desc"> Everything runs on your machine locally. </p> <div class="flex flex-wrap items-center justify-center gap-3"> <a data-os="mac-arm64" href="https://github.com/RhysSullivan/executor/releases/latest/download/executor-desktop-mac-arm64.dmg" class="btn-primary btn-primary--hero" > Download for Mac (Apple Silicon) </a> <a data-os="mac-x64" href="https://github.com/RhysSullivan/executor/releases/latest/download/executor-desktop-mac-x64.dmg" class="btn-primary btn-primary--hero" > Download for Mac (Intel) </a> <a data-os="win-x64" href="https://github.com/RhysSullivan/executor/releases/latest/download/executor-desktop-win-x64.exe" class="btn-primary btn-primary--hero" > Download for Windows </a> <a data-os="linux-x64" href="https://github.com/RhysSullivan/executor/releases/latest/download/executor-desktop-linux-x86_64.AppImage" class="btn-primary btn-primary--hero" > Download for Linux </a> <details class="download-other hidden" data-download-other > <summary>Other platforms ▾</summary> <div class="download-other-list" data-download-other-list > </div> </details> </div> </div> </div> </div> </div> </section> {/* ─── FOUNDER NOTE ─── */} <section class="py-20 sm:py-28 border-t border-rule"> <div class="max-w-[680px] mx-auto px-6 sm:px-10"> <h2 class="text-[clamp(2rem,3.6vw,2.6rem)] font-semibold tracking-[-0.02em] leading-[1.08] text-ink mb-10 text-balance" > What is <span class="serif-italic">Executor</span>? </h2> <div class="space-y-5 text-[16.5px] leading-[1.65] text-ink-2 text-pretty" > <p> Executor is a tool/source discovery and execution layer for agent-callable tools. </p> <p> Executor doesn't care what you add. MCP, OpenAPI, GraphQL, custom sources, under the hood they all become a tool name, an input schema, and an output schema. Once they're in that shape, you can call them however you want. Today that's implemented as a code-mode MCP, but it could just as well be the Executor CLI, or a native client you drop in. </p> <p> Because every tool looks the same, your agent can call them from gen-UI dashboards it writes on the fly, call them as one off scripts in a chat, or use them in reusable workflows. </p> <h3 class="text-[17px] font-semibold tracking-[-0.01em] text-ink pt-5" > Teams </h3> <p> Set your sources up once and your whole team has them. It supports per-user credentials and shared ones. No more onboarding ritual, no more toggling MCPs on and off mid-task. Your agents should be able to reach your company's resources in a way that isn't scary. </p> <h3 class="text-[17px] font-semibold tracking-[-0.01em] text-ink pt-5" > Sandboxing </h3> <p> Tool calls run in a JavaScript sandbox. The agent has access to secrets. </p> <h3 class="text-[17px] font-semibold tracking-[-0.01em] text-ink pt-5" > Destructive actions </h3> <p> Executor preserves the semantics of whatever it imported from: GET vs DELETE for OpenAPI, <code class="font-mono text-[14px] text-ink bg-surface-2 border border-rule px-1.5 py-0.5 rounded" >destructiveHint</code > for MCP, mutations for GraphQL. That tells the agent what it can auto-run and what should pull you back into the loop, so it can work autonomously without taking you out of it. </p> <p class="pt-5"> It's all open source, built on the same SDK we publish to npm. There's two ways to try it, either the local version via the `executor` cli, or the cloud version. </p> <p class="text-ink-3 text-[14.5px] pt-3">— Rhys</p> </div> </div> </section> {/* ─── GET STARTED ─── */} <section id="install" class="py-24 sm:py-32 border-t border-rule"> <div class="max-w-[1200px] mx-auto px-6 sm:px-10"> <div class="mb-14 mx-auto text-center max-w-2xl"> <div class="font-mono text-[12px] uppercase tracking-[0.18em] text-ink-3 mb-4" > Get started </div> <h2 class="text-[clamp(2.2rem,4.5vw,3.4rem)] font-semibold tracking-[-0.02em] leading-[1.05] text-ink mx-auto max-w-[20ch] text-balance" > Pick your <span class="serif-italic">path</span>. </h2> </div> <div class="grid md:grid-cols-2 gap-6 max-w-[760px] mx-auto" > {/* Cloud */} <div class="surface-card p-8 flex flex-col" style="border-color: rgba(99,91,255,0.25)" > <div class="flex items-center gap-2 mb-4"> <span class="font-mono text-[11px] uppercase tracking-[0.18em]" style="color: var(--color-accent)">Recommended</span > </div> <h3 class="text-[22px] font-semibold tracking-[-0.01em] text-ink mb-3" > Cloud </h3> <p class="text-[14.5px] leading-[1.55] text-ink-2 mb-3 text-pretty" > Hosted Executor. Auth, sync, policies, and the whole team online in five minutes. Free tier to start. </p> <p class="mb-6 text-[13.5px] leading-[1.55] text-ink-3 flex-1"> <SelfHostContactModal client:load variant="inline" /> </p> <a href="/cloud" class="btn-primary self-start">Try Cloud →</a> </div> {/* Desktop */} <div class="surface-card p-8 flex flex-col"> <div class="flex items-center gap-2 mb-4"> <span class="font-mono text-[11px] uppercase tracking-[0.18em] text-ink-3" >Open source</span > </div> <h3 class="text-[22px] font-semibold tracking-[-0.01em] text-ink mb-3" > Desktop </h3> <p class="text-[14.5px] leading-[1.55] text-ink-2 mb-6 text-pretty flex-1" > A native app for Mac, Windows, and Linux. Everything stays on your machine — sources, secrets, sessions. MIT licensed. </p> <button type="button" data-jump-to-local class="btn-primary self-start" > Download → </button> </div> </div> </div> </section> {/* ─── FOOTER ─── */} <footer class="border-t border-rule py-10"> <div class="max-w-[1200px] mx-auto px-6 sm:px-10 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6" > <div class="flex items-center gap-2.5"> <img src="/favicon-192.png" alt="" class="w-4 h-4 opacity-80" /> <span class="text-[13px] font-medium text-ink-2">executor</span> <span class="text-[12px] text-ink-3 ml-2" >© {new Date().getFullYear()}</span > </div> <div class="flex items-center gap-6 text-[13px] text-ink-2"> <a href="https://github.com/RhysSullivan/executor" class="hover:text-ink transition-colors">GitHub</a > <a href="/privacy" class="hover:text-ink transition-colors" >Privacy</a > <a href="/terms" class="hover:text-ink transition-colors">Terms</a> </div> </div> </footer> </div> </main></Layout> <script> document .querySelectorAll<HTMLButtonElement>("button[data-copy]") .forEach((btn) => { let timer: ReturnType<typeof setTimeout> | null = null; btn.addEventListener("click", async () => { const text = btn.dataset.copy ?? ""; try { await navigator.clipboard.writeText(text); btn.dataset.copied = "true"; } catch { btn.dataset.copied = "false"; } if (timer) clearTimeout(timer); timer = setTimeout(() => { delete btn.dataset.copied; }, 1500); }); });</script> <script> // ── Hero tab toggle: Try in Cloud / Try locally ──────────────────── // Drives the sliding pill indicator + pane visibility via a single // `data-active` attribute on the wrapper. Default "cloud" so no-JS // visitors still see the recommended path. const heroToggle = document.querySelector<HTMLElement>(".hero-toggle"); const tabToggle = heroToggle?.querySelector<HTMLElement>(".tab-toggle"); const TAB_PARAM = "try"; const isHeroTab = (v: unknown): v is "cloud" | "local" => v === "cloud" || v === "local"; const syncTabToUrl = (tab: "cloud" | "local") => { const url = new URL(window.location.href); if (url.searchParams.get(TAB_PARAM) === tab) return; url.searchParams.set(TAB_PARAM, tab); window.history.replaceState(window.history.state, "", url); }; const setActiveTab = (tab: "cloud" | "local", updateUrl = true) => { if (!heroToggle || !tabToggle) return; heroToggle.dataset.active = tab; tabToggle.dataset.active = tab; tabToggle.querySelectorAll<HTMLButtonElement>("button[data-tab]").forEach( (btn) => { btn.setAttribute( "aria-selected", btn.dataset.tab === tab ? "true" : "false", ); }, ); if (updateUrl) syncTabToUrl(tab); }; if (heroToggle && tabToggle) { const initial = new URLSearchParams(window.location.search).get(TAB_PARAM); if (isHeroTab(initial)) setActiveTab(initial, false); tabToggle.querySelectorAll<HTMLButtonElement>("button[data-tab]").forEach( (btn) => { btn.addEventListener("click", () => { const tab = btn.dataset.tab as "cloud" | "local" | undefined; if (tab) setActiveTab(tab); }); }, ); window.addEventListener("popstate", () => { const next = new URLSearchParams(window.location.search).get(TAB_PARAM); setActiveTab(isHeroTab(next) ? next : "cloud", false); }); } // Desktop card "Download →" CTA jumps to the hero, switches to Local, // and lets the auto-detect script there pick the right OS button. document .querySelectorAll<HTMLButtonElement>("button[data-jump-to-local]") .forEach((btn) => { btn.addEventListener("click", () => { setActiveTab("local"); heroToggle?.scrollIntoView({ behavior: "smooth", block: "center" }); }); }); // ── Auto-detect download platform ────────────────────────────────── // Shows ONE primary button matching the visitor's OS in the "Try // locally" pane; tucks the others behind an "Other platforms" // popover. No-JS / unrecognised UA: all four buttons visible (still // functional, just less polished). type DownloadOs = "mac-arm64" | "mac-x64" | "win-x64" | "linux-x64"; type UserAgentData = { readonly platform: string; readonly getHighEntropyValues?: ( hints: ReadonlyArray<string>, ) => Promise<{ readonly architecture?: string; readonly bitness?: string }>; }; const detectOs = async (): Promise<DownloadOs | null> => { const navAny = navigator as Navigator & { readonly userAgentData?: UserAgentData; }; const uad = navAny.userAgentData; const platformRaw = uad?.platform ?? navigator.platform ?? navigator.userAgent ?? ""; const platform = platformRaw.toLowerCase(); const ua = navigator.userAgent.toLowerCase(); if (platform.includes("win") || ua.includes("windows")) return "win-x64"; if (platform.includes("linux") || ua.includes("linux")) return "linux-x64"; if (platform.includes("mac") || ua.includes("mac")) { // Apple Silicon vs Intel: ask client hints first, then fall back to // a heuristic. Most modern Macs are arm64. if (uad?.getHighEntropyValues) { try { const hints = await uad.getHighEntropyValues([ "architecture", "bitness", ]); if (hints.architecture === "arm") return "mac-arm64"; if (hints.architecture === "x86") return "mac-x64"; } catch {} } return "mac-arm64"; } return null; }; void (async () => { const root = document.querySelector<HTMLElement>("[data-download]"); if (!root) return; const buttons = Array.from( root.querySelectorAll<HTMLAnchorElement>("a[data-os]"), ); const other = root.querySelector<HTMLDetailsElement>( "[data-download-other]", ); const otherList = root.querySelector<HTMLDivElement>( "[data-download-other-list]", ); if (buttons.length === 0 || !other || !otherList) return; const detected = await detectOs(); if (!detected) return; for (const btn of buttons) { const os = btn.dataset.os as DownloadOs | undefined; if (os && os !== detected) { btn.classList.add("hidden"); const clone = btn.cloneNode(true) as HTMLAnchorElement; clone.classList.remove( "btn-primary", "btn-primary--hero", "hidden",Lines 1–500 of 507. Load more
--- import Layout from "../layouts/Layout.astro"; import { AnimatedBeamDemo } from "../components/animated-beam-demo"; import { SelfHostContactModal } from "../components/self-host-contact-modal"; --- <Layout> <main class="bg-soft min-h-dvh relative antialiased"> {/* Pattern + fade — span the top 100vh, behind everything */} <div class="pattern-bg pattern-graph" style="position: absolute; top: 0; left: 0; right: 0; height: 100vh; z-index: 1; pointer-events: none;" > </div> <div style="position: absolute; top: 0; left: 0; right: 0; height: 100vh; z-index: 2; pointer-events: none; background: linear-gradient(180deg, transparent 0%, transparent 65%, var(--color-surface) 100%);" > </div> <div class="relative z-10"> {/* NAV */} <nav> <div class="max-w-[1200px] mx-auto px-6 sm:px-10 h-14 flex items-center justify-between" > <a href="/" class="flex items-center gap-2.5"> <img src="/favicon-192.png" alt="" class="w-5 h-5" /> <span class="text-[15px] font-semibold tracking-[-0.01em] text-ink" >executor</span > </a> <div class="flex items-center gap-5"> <a href="https://github.com/RhysSullivan/executor" class="text-[14px] font-medium text-ink-2 hover:text-ink transition-colors" >GitHub</a > <a href="/cloud" class="btn-primary">Try Cloud →</a> </div> </div> </nav> {/* HERO — centered, tight */} <section class="relative"> <div class="relative z-10 px-6 sm:px-10 pt-28 pb-32 sm:pt-36 sm:pb-44"> <div class="max-w-[920px] mx-auto text-center"> <h1 class="rise-1 text-[clamp(2.8rem,7.2vw,6rem)] font-semibold tracking-[-0.025em] leading-[1.02] text-ink mb-12 mx-auto max-w-[18ch] text-balance" > Connect any agent to <span class="serif-italic">everything</span>. </h1> <div class="rise-2 max-w-[760px] mx-auto mb-12"> <div class="surface-card p-6 sm:p-7"> <AnimatedBeamDemo client:load variant="stripe" /> </div> </div> <p class="rise-3 text-[18px] leading-[1.55] text-ink-2 mx-auto max-w-[44ch] mb-9 text-pretty" > One gateway. Every tool. Every team. </p> {/* ── Toggle: Try in Cloud / Try locally ── */} <div class="rise-4 hero-toggle flex flex-col items-center gap-7" data-active="cloud" > <div class="tab-toggle" data-active="cloud" role="tablist" aria-label="Choose how to try Executor" > <span class="tab-indicator" aria-hidden="true"></span> <button type="button" role="tab" aria-selected="true" data-tab="cloud" > Cloud </button> <button type="button" role="tab" aria-selected="false" data-tab="local" > Local </button> </div> {/* Cloud pane */} <div class="pane flex-col items-center gap-4" data-pane="cloud" role="tabpanel" aria-labelledby="tab-cloud" > <p class="pane-desc"> Built for teams. Plug into any agent via MCP. </p> <a href="/cloud" class="btn-primary btn-primary--hero" >Try Cloud →</a > </div> {/* Local pane — auto-detected download */} <div class="pane flex-col items-center gap-4" data-pane="local" role="tabpanel" aria-labelledby="tab-local" data-download > <p class="pane-desc"> Everything runs on your machine locally. </p> <div class="flex flex-wrap items-center justify-center gap-3"> <a data-os="mac-arm64" href="https://github.com/RhysSullivan/executor/releases/latest/download/executor-desktop-mac-arm64.dmg" class="btn-primary btn-primary--hero" > Download for Mac (Apple Silicon) </a> <a data-os="mac-x64" href="https://github.com/RhysSullivan/executor/releases/latest/download/executor-desktop-mac-x64.dmg" class="btn-primary btn-primary--hero" > Download for Mac (Intel) </a> <a data-os="win-x64" href="https://github.com/RhysSullivan/executor/releases/latest/download/executor-desktop-win-x64.exe" class="btn-primary btn-primary--hero" > Download for Windows </a> <a data-os="linux-x64" href="https://github.com/RhysSullivan/executor/releases/latest/download/executor-desktop-linux-x86_64.AppImage" class="btn-primary btn-primary--hero" > Download for Linux </a> <details class="download-other hidden" data-download-other > <summary>Other platforms ▾</summary> <div class="download-other-list" data-download-other-list > </div> </details> </div> </div> </div> </div> </div> </section> {/* ─── FOUNDER NOTE ─── */} <section class="py-20 sm:py-28 border-t border-rule"> <div class="max-w-[680px] mx-auto px-6 sm:px-10"> <h2 class="text-[clamp(2rem,3.6vw,2.6rem)] font-semibold tracking-[-0.02em] leading-[1.08] text-ink mb-10 text-balance" > What is <span class="serif-italic">Executor</span>? </h2> <div class="space-y-5 text-[16.5px] leading-[1.65] text-ink-2 text-pretty" > <p> Executor is a tool/source discovery and execution layer for agent-callable tools. </p> <p> Executor doesn't care what you add. MCP, OpenAPI, GraphQL, custom sources, under the hood they all become a tool name, an input schema, and an output schema. Once they're in that shape, you can call them however you want. Today that's implemented as a code-mode MCP, but it could just as well be the Executor CLI, or a native client you drop in. </p> <p> Because every tool looks the same, your agent can call them from gen-UI dashboards it writes on the fly, call them as one off scripts in a chat, or use them in reusable workflows. </p> <h3 class="text-[17px] font-semibold tracking-[-0.01em] text-ink pt-5" > Teams </h3> <p> Set your sources up once and your whole team has them. It supports per-user credentials and shared ones. No more onboarding ritual, no more toggling MCPs on and off mid-task. Your agents should be able to reach your company's resources in a way that isn't scary. </p> <h3 class="text-[17px] font-semibold tracking-[-0.01em] text-ink pt-5" > Sandboxing </h3> <p> Tool calls run in a JavaScript sandbox. The agent has access to secrets. </p> <h3 class="text-[17px] font-semibold tracking-[-0.01em] text-ink pt-5" > Destructive actions </h3> <p> Executor preserves the semantics of whatever it imported from: GET vs DELETE for OpenAPI, <code class="font-mono text-[14px] text-ink bg-surface-2 border border-rule px-1.5 py-0.5 rounded" >destructiveHint</code > for MCP, mutations for GraphQL. That tells the agent what it can auto-run and what should pull you back into the loop, so it can work autonomously without taking you out of it. </p> <p class="pt-5"> It's all open source, built on the same SDK we publish to npm. There's two ways to try it, either the local version via the `executor` cli, or the cloud version. </p> <p class="text-ink-3 text-[14.5px] pt-3">— Rhys</p> </div> </div> </section> {/* ─── GET STARTED ─── */} <section id="install" class="py-24 sm:py-32 border-t border-rule"> <div class="max-w-[1200px] mx-auto px-6 sm:px-10"> <div class="mb-14 mx-auto text-center max-w-2xl"> <div class="font-mono text-[12px] uppercase tracking-[0.18em] text-ink-3 mb-4" > Get started </div> <h2 class="text-[clamp(2.2rem,4.5vw,3.4rem)] font-semibold tracking-[-0.02em] leading-[1.05] text-ink mx-auto max-w-[20ch] text-balance" > Pick your <span class="serif-italic">path</span>. </h2> </div> <div class="grid md:grid-cols-2 gap-6 max-w-[760px] mx-auto" > {/* Cloud */} <div class="surface-card p-8 flex flex-col" style="border-color: rgba(99,91,255,0.25)" > <div class="flex items-center gap-2 mb-4"> <span class="font-mono text-[11px] uppercase tracking-[0.18em]" style="color: var(--color-accent)">Recommended</span > </div> <h3 class="text-[22px] font-semibold tracking-[-0.01em] text-ink mb-3" > Cloud </h3> <p class="text-[14.5px] leading-[1.55] text-ink-2 mb-3 text-pretty" > Hosted Executor. Auth, sync, policies, and the whole team online in five minutes. Free tier to start. </p> <p class="mb-6 text-[13.5px] leading-[1.55] text-ink-3 flex-1"> <SelfHostContactModal client:load variant="inline" /> </p> <a href="/cloud" class="btn-primary self-start">Try Cloud →</a> </div> {/* Desktop */} <div class="surface-card p-8 flex flex-col"> <div class="flex items-center gap-2 mb-4"> <span class="font-mono text-[11px] uppercase tracking-[0.18em] text-ink-3" >Open source</span > </div> <h3 class="text-[22px] font-semibold tracking-[-0.01em] text-ink mb-3" > Desktop </h3> <p class="text-[14.5px] leading-[1.55] text-ink-2 mb-6 text-pretty flex-1" > A native app for Mac, Windows, and Linux. Everything stays on your machine — sources, secrets, sessions. MIT licensed. </p> <button type="button" data-jump-to-local class="btn-primary self-start" > Download → </button> </div> </div> </div> </section> {/* ─── FOOTER ─── */} <footer class="border-t border-rule py-10"> <div class="max-w-[1200px] mx-auto px-6 sm:px-10 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6" > <div class="flex items-center gap-2.5"> <img src="/favicon-192.png" alt="" class="w-4 h-4 opacity-80" /> <span class="text-[13px] font-medium text-ink-2">executor</span> <span class="text-[12px] text-ink-3 ml-2" >© {new Date().getFullYear()}</span > </div> <div class="flex items-center gap-6 text-[13px] text-ink-2"> <a href="https://github.com/RhysSullivan/executor" class="hover:text-ink transition-colors">GitHub</a > <a href="/privacy" class="hover:text-ink transition-colors" >Privacy</a > <a href="/terms" class="hover:text-ink transition-colors">Terms</a> </div> </div> </footer> </div> </main> </Layout> <script> document .querySelectorAll<HTMLButtonElement>("button[data-copy]") .forEach((btn) => { let timer: ReturnType<typeof setTimeout> | null = null; btn.addEventListener("click", async () => { const text = btn.dataset.copy ?? ""; try { await navigator.clipboard.writeText(text); btn.dataset.copied = "true"; } catch { btn.dataset.copied = "false"; } if (timer) clearTimeout(timer); timer = setTimeout(() => { delete btn.dataset.copied; }, 1500); }); }); </script> <script> // ── Hero tab toggle: Try in Cloud / Try locally ──────────────────── // Drives the sliding pill indicator + pane visibility via a single // `data-active` attribute on the wrapper. Default "cloud" so no-JS // visitors still see the recommended path. const heroToggle = document.querySelector<HTMLElement>(".hero-toggle"); const tabToggle = heroToggle?.querySelector<HTMLElement>(".tab-toggle"); const TAB_PARAM = "try"; const isHeroTab = (v: unknown): v is "cloud" | "local" => v === "cloud" || v === "local"; const syncTabToUrl = (tab: "cloud" | "local") => { const url = new URL(window.location.href); if (url.searchParams.get(TAB_PARAM) === tab) return; url.searchParams.set(TAB_PARAM, tab); window.history.replaceState(window.history.state, "", url); }; const setActiveTab = (tab: "cloud" | "local", updateUrl = true) => { if (!heroToggle || !tabToggle) return; heroToggle.dataset.active = tab; tabToggle.dataset.active = tab; tabToggle.querySelectorAll<HTMLButtonElement>("button[data-tab]").forEach( (btn) => { btn.setAttribute( "aria-selected", btn.dataset.tab === tab ? "true" : "false", ); }, ); if (updateUrl) syncTabToUrl(tab); }; if (heroToggle && tabToggle) { const initial = new URLSearchParams(window.location.search).get(TAB_PARAM); if (isHeroTab(initial)) setActiveTab(initial, false); tabToggle.querySelectorAll<HTMLButtonElement>("button[data-tab]").forEach( (btn) => { btn.addEventListener("click", () => { const tab = btn.dataset.tab as "cloud" | "local" | undefined; if (tab) setActiveTab(tab); }); }, ); window.addEventListener("popstate", () => { const next = new URLSearchParams(window.location.search).get(TAB_PARAM); setActiveTab(isHeroTab(next) ? next : "cloud", false); }); } // Desktop card "Download →" CTA jumps to the hero, switches to Local, // and lets the auto-detect script there pick the right OS button. document .querySelectorAll<HTMLButtonElement>("button[data-jump-to-local]") .forEach((btn) => { btn.addEventListener("click", () => { setActiveTab("local"); heroToggle?.scrollIntoView({ behavior: "smooth", block: "center" }); }); }); // ── Auto-detect download platform ────────────────────────────────── // Shows ONE primary button matching the visitor's OS in the "Try // locally" pane; tucks the others behind an "Other platforms" // popover. No-JS / unrecognised UA: all four buttons visible (still // functional, just less polished). type DownloadOs = "mac-arm64" | "mac-x64" | "win-x64" | "linux-x64"; type UserAgentData = { readonly platform: string; readonly getHighEntropyValues?: ( hints: ReadonlyArray<string>, ) => Promise<{ readonly architecture?: string; readonly bitness?: string }>; }; const detectOs = async (): Promise<DownloadOs | null> => { const navAny = navigator as Navigator & { readonly userAgentData?: UserAgentData; }; const uad = navAny.userAgentData; const platformRaw = uad?.platform ?? navigator.platform ?? navigator.userAgent ?? ""; const platform = platformRaw.toLowerCase(); const ua = navigator.userAgent.toLowerCase(); if (platform.includes("win") || ua.includes("windows")) return "win-x64"; if (platform.includes("linux") || ua.includes("linux")) return "linux-x64"; if (platform.includes("mac") || ua.includes("mac")) { // Apple Silicon vs Intel: ask client hints first, then fall back to // a heuristic. Most modern Macs are arm64. if (uad?.getHighEntropyValues) { try { const hints = await uad.getHighEntropyValues([ "architecture", "bitness", ]); if (hints.architecture === "arm") return "mac-arm64"; if (hints.architecture === "x86") return "mac-x64"; } catch {} } return "mac-arm64"; } return null; }; void (async () => { const root = document.querySelector<HTMLElement>("[data-download]"); if (!root) return; const buttons = Array.from( root.querySelectorAll<HTMLAnchorElement>("a[data-os]"), ); const other = root.querySelector<HTMLDetailsElement>( "[data-download-other]", ); const otherList = root.querySelector<HTMLDivElement>( "[data-download-other-list]", ); if (buttons.length === 0 || !other || !otherList) return; const detected = await detectOs(); if (!detected) return; for (const btn of buttons) { const os = btn.dataset.os as DownloadOs | undefined; if (os && os !== detected) { btn.classList.add("hidden"); const clone = btn.cloneNode(true) as HTMLAnchorElement; clone.classList.remove( "btn-primary", "btn-primary--hero", "hidden",