| author | |
| committer | |
| log | 67e2fe71d937b45384a53c4e0addd9a167aed885 |
| tree | 28db89d13db7dbdfea3af102172a78c3bb1ed851 |
| parent | 3b8e7bbbe0c49b00630f0c89e931056df679a650 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>4 files changed, 30 insertions(+), 69 deletions(-)
apps/marketing/src/lib/macArch.test.ts deleted-23| ... | @@ -1,23 +0,0 @@ | ||
| 1 | import { describe, expect, it } from "vite-plus/test"; | ||
| 2 | |||
| 3 | import { macArchFromGpuRenderer } from "./macArch"; | ||
| 4 | |||
| 5 | describe("macArchFromGpuRenderer", () => { | ||
| 6 | it("detects explicit Apple Silicon renderers", () => { | ||
| 7 | expect(macArchFromGpuRenderer("Apple M4 Pro")).toBe("arm64"); | ||
| 8 | }); | ||
| 9 | |||
| 10 | it("prefers an Intel GPU marker even when the renderer also mentions Apple", () => { | ||
| 11 | expect(macArchFromGpuRenderer("ANGLE Metal Renderer: Apple, Intel Iris Plus Graphics")).toBe( | ||
| 12 | "x64", | ||
| 13 | ); | ||
| 14 | }); | ||
| 15 | |||
| 16 | it("detects Safari's generic Apple Silicon renderer", () => { | ||
| 17 | expect(macArchFromGpuRenderer("Apple GPU")).toBe("arm64"); | ||
| 18 | }); | ||
| 19 | |||
| 20 | it("uses x64 when the renderer is unavailable", () => { | ||
| 21 | expect(macArchFromGpuRenderer("")).toBe("x64"); | ||
| 22 | }); | ||
| 23 | }); | ||
apps/marketing/src/lib/macArch.ts deleted-14| ... | @@ -1,14 +0,0 @@ | ||
| 1 | const INTEL_GPU_PATTERN = /intel|amd|radeon|nvidia|geforce/i; | ||
| 2 | const APPLE_SILICON_GPU_PATTERN = /\bapple\s+(?:m\d|gpu)\b/i; | ||
| 3 | |||
| 4 | export function macArchFromGpuRenderer(renderer: string): "arm64" | "x64" { | ||
| 5 | if (INTEL_GPU_PATTERN.test(renderer)) { | ||
| 6 | return "x64"; | ||
| 7 | } | ||
| 8 | if (APPLE_SILICON_GPU_PATTERN.test(renderer)) { | ||
| 9 | return "arm64"; | ||
| 10 | } | ||
| 11 | |||
| 12 | // Keep the fallback compatible with Intel Macs when WebGL is unavailable. | ||
| 13 | return "x64"; | ||
| 14 | } | ||
apps/marketing/src/pages/download.astro+23-6| ... | @@ -24,11 +24,10 @@ import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site"; | ... | @@ -24,11 +24,10 @@ import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site"; |
| 24 | <span class="card-arch">Apple Silicon (arm64)</span> | 24 | <span class="card-arch">Apple Silicon (arm64)</span> |
| 25 | <span class="card-format">.dmg</span> | 25 | <span class="card-format">.dmg</span> |
| 26 | </a> | 26 | </a> |
| 27 | <a class="download-card" data-asset="x64.dmg" href={RELEASES_URL}> | ||
| 28 | <span class="card-arch">Intel (x64)</span> | ||
| 29 | <span class="card-format">.dmg</span> | ||
| 30 | </a> | ||
| 31 | </div> | 27 | </div> |
| 28 | <p class="intel-note"> | ||
| 29 | On an Intel Mac? <a data-asset="x64.dmg" href={RELEASES_URL}>Download the x64 build</a>. | ||
| 30 | </p> | ||
| 32 | </section> | 31 | </section> |
| 33 | 32 | ||
| 34 | <!-- Windows --> | 33 | <!-- Windows --> |
| ... | @@ -92,8 +91,8 @@ import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site"; | ... | @@ -92,8 +91,8 @@ import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site"; |
| 92 | 91 | ||
| 93 | async function init() { | 92 | async function init() { |
| 94 | const versionLabel = document.getElementById("version-label"); | 93 | const versionLabel = document.getElementById("version-label"); |
| 95 | // Only release-asset cards; mobile store cards have no data-asset and keep their href. | 94 | // Only release-asset links; mobile store cards have no data-asset and keep their href. |
| 96 | const cards = document.querySelectorAll<HTMLAnchorElement>(".download-card[data-asset]"); | 95 | const cards = document.querySelectorAll<HTMLAnchorElement>("a[data-asset]"); |
| 97 | 96 | ||
| 98 | try { | 97 | try { |
| 99 | const release = await fetchLatestRelease(); | 98 | const release = await fetchLatestRelease(); |
| ... | @@ -257,6 +256,24 @@ import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site"; | ... | @@ -257,6 +256,24 @@ import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site"; |
| 257 | color: var(--fg-dim); | 256 | color: var(--fg-dim); |
| 258 | } | 257 | } |
| 259 | 258 | ||
| 259 | .intel-note { | ||
| 260 | font-size: 0.8rem; | ||
| 261 | color: var(--fg-dim); | ||
| 262 | } | ||
| 263 | |||
| 264 | .intel-note a { | ||
| 265 | color: var(--fg-muted); | ||
| 266 | text-decoration: underline; | ||
| 267 | text-decoration-color: rgba(161, 161, 170, 0.4); | ||
| 268 | text-underline-offset: 3px; | ||
| 269 | transition: color 0.3s ease, text-decoration-color 0.3s ease; | ||
| 270 | } | ||
| 271 | |||
| 272 | .intel-note a:hover { | ||
| 273 | color: var(--fg); | ||
| 274 | text-decoration-color: var(--fg); | ||
| 275 | } | ||
| 276 | |||
| 260 | /* ── Releases link ── */ | 277 | /* ── Releases link ── */ |
| 261 | 278 | ||
| 262 | .releases-link { | 279 | .releases-link { |
apps/marketing/src/pages/index.astro+7-26| ... | @@ -391,46 +391,27 @@ const mobileEndorsementRows = [ | ... | @@ -391,46 +391,27 @@ const mobileEndorsementRows = [ |
| 391 | 391 | ||
| 392 | <script> | 392 | <script> |
| 393 | import { fetchLatestRelease, RELEASES_URL, type ReleaseAsset } from "../lib/releases"; | 393 | import { fetchLatestRelease, RELEASES_URL, type ReleaseAsset } from "../lib/releases"; |
| 394 | import { macArchFromGpuRenderer } from "../lib/macArch"; | ||
| 395 | 394 | ||
| 396 | type Platform = { os: "mac" | "win" | "linux"; label: string; arch?: string }; | 395 | type Platform = { os: "mac" | "win" | "linux"; label: string }; |
| 397 | |||
| 398 | // navigator.userAgent reports "Intel Mac OS X" on both Intel and Apple Silicon | ||
| 399 | // Macs, so probe the GPU renderer instead: Apple Silicon exposes an "Apple" GPU. | ||
| 400 | function detectMacArch(): "arm64" | "x64" { | ||
| 401 | try { | ||
| 402 | const canvas = document.createElement("canvas"); | ||
| 403 | const gl = (canvas.getContext("webgl") || | ||
| 404 | canvas.getContext("experimental-webgl")) as WebGLRenderingContext | null; | ||
| 405 | const debugInfo = gl?.getExtension("WEBGL_debug_renderer_info"); | ||
| 406 | if (gl && debugInfo) { | ||
| 407 | const renderer = String(gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) ?? ""); | ||
| 408 | return macArchFromGpuRenderer(renderer); | ||
| 409 | } | ||
| 410 | } catch { | ||
| 411 | // fall through to the default below | ||
| 412 | } | ||
| 413 | return macArchFromGpuRenderer(""); | ||
| 414 | } | ||
| 415 | 396 | ||
| 416 | function detectPlatform(): Platform | null { | 397 | function detectPlatform(): Platform | null { |
| 417 | const ua = navigator.userAgent; | 398 | const ua = navigator.userAgent; |
| 418 | if (/Win/i.test(ua)) return { os: "win", label: "Download for Windows" }; | 399 | if (/Win/i.test(ua)) return { os: "win", label: "Download for Windows" }; |
| 419 | if (/Mac/i.test(ua)) { | 400 | if (/Mac/i.test(ua)) return { os: "mac", label: "Download for macOS" }; |
| 420 | return { os: "mac", label: "Download for macOS", arch: detectMacArch() }; | ||
| 421 | } | ||
| 422 | if (/Linux/i.test(ua)) return { os: "linux", label: "Download for Linux" }; | 401 | if (/Linux/i.test(ua)) return { os: "linux", label: "Download for Linux" }; |
| 423 | return null; | 402 | return null; |
| 424 | } | 403 | } |
| 425 | 404 | ||
| 405 | // Macs always get the arm64 build. Browsers cannot reliably tell Apple Silicon | ||
| 406 | // from Intel (the UA lies, GPU strings lie), and a wrong guess hands an Apple | ||
| 407 | // Silicon user a broken app. Intel users pick their build on /download. | ||
| 408 | // Do NOT add arch detection back here. | ||
| 426 | function pickAsset(assets: ReleaseAsset[], platform: Platform): string | null { | 409 | function pickAsset(assets: ReleaseAsset[], platform: Platform): string | null { |
| 427 | if (platform.os === "win") { | 410 | if (platform.os === "win") { |
| 428 | return assets.find((a) => a.name.endsWith("-x64.exe"))?.browser_download_url ?? null; | 411 | return assets.find((a) => a.name.endsWith("-x64.exe"))?.browser_download_url ?? null; |
| 429 | } | 412 | } |
| 430 | if (platform.os === "mac") { | 413 | if (platform.os === "mac") { |
| 431 | const preferred = assets.find((a) => a.name.endsWith(`-${platform.arch}.dmg`)); | 414 | return assets.find((a) => a.name.endsWith("-arm64.dmg"))?.browser_download_url ?? null; |
| 432 | const fallback = assets.find((a) => a.name.endsWith(".dmg")); | ||
| 433 | return (preferred ?? fallback)?.browser_download_url ?? null; | ||
| 434 | } | 415 | } |
| 435 | if (platform.os === "linux") { | 416 | if (platform.os === "linux") { |
| 436 | return assets.find((a) => a.name.endsWith(".AppImage"))?.browser_download_url ?? null; | 417 | return assets.find((a) => a.name.endsWith(".AppImage"))?.browser_download_url ?? null; |