fix(web): add macOS Intel downloads to landing page (#5517)

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Jiayuan Zhang
2026-07-16 15:11:27 +08:00
committed by GitHub
parent 6b2097ccbb
commit 47e9c8add5
12 changed files with 197 additions and 54 deletions

View File

@@ -0,0 +1,57 @@
import { render, screen, within } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { AllPlatforms } from "./all-platforms";
vi.mock("../../i18n", () => ({
useLocale: () => ({
t: {
download: {
allPlatforms: {
title: "All platforms",
macArm64Label: "macOS · Apple Silicon",
macX64Label: "macOS · Intel",
winX64Label: "Windows · x64",
winArm64Label: "Windows · ARM64",
linuxX64Label: "Linux · x64",
linuxArm64Label: "Linux · ARM64",
formatDmg: ".dmg",
formatZip: ".zip",
formatExe: ".exe",
formatAppImage: ".AppImage",
formatDeb: ".deb",
formatRpm: ".rpm",
unavailable: "Not available",
},
footer: { allReleases: "View all releases" },
},
},
}),
}));
describe("AllPlatforms", () => {
it("lists Intel macOS downloads separately from Apple Silicon", () => {
render(
<AllPlatforms
assets={{
macArm64Dmg: "https://downloads.test/mac-arm64.dmg",
macArm64Zip: "https://downloads.test/mac-arm64.zip",
macX64Dmg: "https://downloads.test/mac-x64.dmg",
macX64Zip: "https://downloads.test/mac-x64.zip",
}}
fallbackHref="https://github.test/releases"
/>,
);
const intelLabel = screen.getByText("macOS · Intel");
const intelRow = intelLabel.parentElement?.parentElement;
expect(intelRow).not.toBeNull();
expect(within(intelRow!).getByRole("link", { name: ".dmg" })).toHaveAttribute(
"href",
"https://downloads.test/mac-x64.dmg",
);
expect(within(intelRow!).getByRole("link", { name: ".zip" })).toHaveAttribute(
"href",
"https://downloads.test/mac-x64.zip",
);
});
});

View File

@@ -35,7 +35,7 @@ export function AllPlatforms({
<div className="mt-10 overflow-hidden rounded-2xl border border-[#0a0d12]/10">
<Row
icon={<AppleIcon className="text-[#0a0d12]" />}
label={d.macLabel}
label={d.macArm64Label}
formats={[
{
label: d.formatDmg,
@@ -48,6 +48,21 @@ export function AllPlatforms({
]}
unavailable={d.unavailable}
/>
<Row
icon={<AppleIcon className="text-[#0a0d12]" />}
label={d.macX64Label}
formats={[
{
label: d.formatDmg,
href: assets.macX64Dmg,
},
{
label: d.formatZip,
href: assets.macX64Zip,
},
]}
unavailable={d.unavailable}
/>
<Row
icon={<WindowsIcon className="text-[#0a0d12]" />}
label={d.winX64Label}
@@ -111,10 +126,8 @@ export function AllPlatforms({
/>
</div>
<p className="mt-6 text-[13px] text-[#0a0d12]/60">{d.intelNote}</p>
{isFallbackNeeded(assets) ? (
<p className="mt-2 text-[13px] text-[#0a0d12]/60">
<p className="mt-6 text-[13px] text-[#0a0d12]/60">
<Link
href={fallbackHref}
className="underline decoration-[#0a0d12]/30 underline-offset-4 hover:text-[#0a0d12] hover:decoration-[#0a0d12]/70"
@@ -182,10 +195,10 @@ function Row({ icon, label, formats, unavailable, isLast }: RowProps) {
);
}
// Ten desktop artifacts are expected per release (two Mac,
// Twelve desktop artifacts are expected per release (four Mac,
// two Windows, six Linux). If any are missing, surface the GitHub
// fallback link so users on an orphaned row have a way out.
const EXPECTED_ASSET_COUNT = 10;
const EXPECTED_ASSET_COUNT = 12;
function isFallbackNeeded(assets: DownloadAssets): boolean {
return Object.values(assets).filter(Boolean).length < EXPECTED_ASSET_COUNT;

View File

@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import { createEnDict } from "../../i18n/en";
import { resolveContent } from "./hero";
describe("resolveContent", () => {
it("links confidently detected Intel Macs to the x64 installers", () => {
const content = resolveContent(
{ os: "mac", arch: "x64", archConfident: true },
{
macArm64Dmg: "https://downloads.test/mac-arm64.dmg",
macArm64Zip: "https://downloads.test/mac-arm64.zip",
macX64Dmg: "https://downloads.test/mac-x64.dmg",
macX64Zip: "https://downloads.test/mac-x64.zip",
},
false,
createEnDict(true).download.hero,
);
expect(content.primary).toEqual({
href: "https://downloads.test/mac-x64.dmg",
label: "Download (.dmg)",
disabled: false,
});
expect(content.alt).toEqual({
href: "https://downloads.test/mac-x64.zip",
label: "or download .zip",
});
});
});

View File

@@ -96,7 +96,7 @@ interface HeroContent {
type HeroDict = ReturnType<typeof useLocale>["t"]["download"]["hero"];
function resolveContent(
export function resolveContent(
detected: DetectResult | null,
assets: DownloadAssets,
versionUnavailable: boolean,
@@ -111,17 +111,28 @@ function resolveContent(
if (detected.os === "mac") {
// Only Chromium high-entropy returns arch confidently. Safari
// always reports Intel even on Apple Silicon, so we treat
// "non-confident" as arm64 + add a small Intel disclaimer.
// "non-confident" as arm64 + point Intel users to the matrix below.
if (detected.arch === "x64" && detected.archConfident) {
const dmg = assets.macX64Dmg;
const zip = assets.macX64Zip;
return {
title: d.macIntel.title,
sub: d.macIntel.sub,
primary: {
href: "#cli",
label: d.macIntel.disabledCta,
disabled: true,
},
hint: d.macIntel.intelHint,
primary: dmg
? {
href: dmg,
label: d.macIntel.primary,
disabled: false,
}
: versionUnavailable
? { href: "#", label: d.macIntel.primary, disabled: true }
: undefined,
alt: zip
? {
href: zip,
label: d.macIntel.altZip,
}
: undefined,
};
}
const dmg = assets.macArm64Dmg;

View File

@@ -2271,10 +2271,9 @@ export function createEnDict(allowSignup: boolean): LandingDict {
},
macIntel: {
title: "Multica for macOS",
sub: "Apple Silicon required — Intel Macs not yet supported.",
disabledCta: "Apple Silicon required",
intelHint:
"On an Intel Mac? Use the CLI below — it runs the same daemon.",
sub: "Intel · bundled daemon, zero setup",
primary: "Download (.dmg)",
altZip: "or download .zip",
},
winX64: {
title: "Multica for Windows",
@@ -2296,12 +2295,13 @@ export function createEnDict(allowSignup: boolean): LandingDict {
title: "Choose your platform",
sub: "All installers are listed below.",
},
safariMacHint: "On an Intel Mac? Use the CLI below.",
safariMacHint: "On an Intel Mac? Choose the Intel download below.",
archFallbackHint: "Wrong architecture? See all formats below.",
},
allPlatforms: {
title: "All platforms",
macLabel: "macOS · Apple Silicon",
macArm64Label: "macOS · Apple Silicon",
macX64Label: "macOS · Intel",
winX64Label: "Windows · x64",
winArm64Label: "Windows · ARM64",
linuxX64Label: "Linux · x64",
@@ -2312,8 +2312,6 @@ export function createEnDict(allowSignup: boolean): LandingDict {
formatAppImage: ".AppImage",
formatDeb: ".deb",
formatRpm: ".rpm",
intelNote:
"Apple Silicon only — Intel Macs not supported in this release.",
unavailable: "Not available",
},
cli: {

View File

@@ -1773,10 +1773,9 @@ export function createJaDict(allowSignup: boolean): LandingDict {
},
macIntel: {
title: "macOS 版 Multica",
sub: "Apple Silicon が必要です。Intel Mac はまだ対応していません。",
disabledCta: "Apple Silicon が必要",
intelHint:
"Intel Mac をお使いですか? 下の CLI をご利用ください。同じデーモンが動きます。",
sub: "Intel · デーモン同梱、設定不要",
primary: "ダウンロード(.dmg)",
altZip: "または .zip をダウンロード",
},
winX64: {
title: "Windows 版 Multica",
@@ -1798,12 +1797,13 @@ export function createJaDict(allowSignup: boolean): LandingDict {
title: "プラットフォームを選択",
sub: "すべてのインストーラーは下にまとまっています。",
},
safariMacHint: "Intel Mac をお使いですか? 下の CLI をご利用ください。",
safariMacHint: "Intel Mac をお使いですか? 下の Intel 版を選択してください。",
archFallbackHint: "アーキテクチャが合いませんか? 下ですべての形式を確認してください。",
},
allPlatforms: {
title: "すべてのプラットフォーム",
macLabel: "macOS · Apple Silicon",
macArm64Label: "macOS · Apple Silicon",
macX64Label: "macOS · Intel",
winX64Label: "Windows · x64",
winArm64Label: "Windows · ARM64",
linuxX64Label: "Linux · x64",
@@ -1814,8 +1814,6 @@ export function createJaDict(allowSignup: boolean): LandingDict {
formatAppImage: ".AppImage",
formatDeb: ".deb",
formatRpm: ".rpm",
intelNote:
"Apple Silicon のみ対応です。このリリースでは Intel Mac には対応していません。",
unavailable: "利用できません",
},
cli: {

View File

@@ -1798,10 +1798,9 @@ export function createKoDict(allowSignup: boolean): LandingDict {
},
macIntel: {
title: "macOS용 Multica",
sub: "Apple Silicon이 필요합니다. Intel Mac은 아직 지원하지 않습니다.",
disabledCta: "Apple Silicon 필요",
intelHint:
"Intel Mac을 사용 중인가요? 아래 CLI를 사용하세요. 같은 데몬이 실행됩니다.",
sub: "Intel · 데몬 포함, 별도 설정 없음",
primary: "다운로드(.dmg)",
altZip: "또는 .zip 다운로드",
},
winX64: {
title: "Windows용 Multica",
@@ -1823,12 +1822,13 @@ export function createKoDict(allowSignup: boolean): LandingDict {
title: "플랫폼 선택",
sub: "모든 설치 파일은 아래에 정리되어 있습니다.",
},
safariMacHint: "Intel Mac을 사용 중인가요? 아래 CLI를 사용하세요.",
safariMacHint: "Intel Mac을 사용 중인가요? 아래에서 Intel 버전을 선택하세요.",
archFallbackHint: "아키텍처가 맞지 않나요? 아래에서 모든 형식을 확인하세요.",
},
allPlatforms: {
title: "모든 플랫폼",
macLabel: "macOS · Apple Silicon",
macArm64Label: "macOS · Apple Silicon",
macX64Label: "macOS · Intel",
winX64Label: "Windows · x64",
winArm64Label: "Windows · ARM64",
linuxX64Label: "Linux · x64",
@@ -1839,8 +1839,6 @@ export function createKoDict(allowSignup: boolean): LandingDict {
formatAppImage: ".AppImage",
formatDeb: ".deb",
formatRpm: ".rpm",
intelNote:
"Apple Silicon만 지원합니다. 이번 릴리스에서는 Intel Mac을 지원하지 않습니다.",
unavailable: "사용할 수 없음",
},
cli: {

View File

@@ -145,8 +145,8 @@ export type LandingDict = {
macIntel: {
title: string;
sub: string;
disabledCta: string;
intelHint: string;
primary: string;
altZip: string;
};
winX64: { title: string; sub: string; primary: string };
winArm64: { title: string; sub: string; primary: string };
@@ -162,7 +162,8 @@ export type LandingDict = {
};
allPlatforms: {
title: string;
macLabel: string;
macArm64Label: string;
macX64Label: string;
winX64Label: string;
winArm64Label: string;
linuxX64Label: string;
@@ -173,7 +174,6 @@ export type LandingDict = {
formatAppImage: string;
formatDeb: string;
formatRpm: string;
intelNote: string;
unavailable: string;
};
cli: {

View File

@@ -2270,9 +2270,9 @@ export function createZhDict(allowSignup: boolean): LandingDict {
},
macIntel: {
title: "Multica for macOS",
sub: "需要 Apple Silicon——暂不支持 Intel Mac。",
disabledCta: "需要 Apple Silicon",
intelHint: "在 Intel Mac 上?请使用下方 CLI——底层跑的是同一个 daemon。",
sub: "Intel · 内置守护进程,无需配置",
primary: "下载 (.dmg)",
altZip: "或下载 .zip",
},
winX64: {
title: "Multica for Windows",
@@ -2294,12 +2294,13 @@ export function createZhDict(allowSignup: boolean): LandingDict {
title: "选择你的平台",
sub: "下方是所有支持的安装包。",
},
safariMacHint: "在 Intel Mac 上?请使用下方 CLI。",
safariMacHint: "在 Intel Mac 上?请在下方选择 Intel 版本。",
archFallbackHint: "架构不对?下方是所有可选格式。",
},
allPlatforms: {
title: "所有平台",
macLabel: "macOS · Apple Silicon",
macArm64Label: "macOS · Apple Silicon",
macX64Label: "macOS · Intel",
winX64Label: "Windows · x64",
winArm64Label: "Windows · ARM64",
linuxX64Label: "Linux · x64",
@@ -2310,7 +2311,6 @@ export function createZhDict(allowSignup: boolean): LandingDict {
formatAppImage: ".AppImage",
formatDeb: ".deb",
formatRpm: ".rpm",
intelNote: "仅支持 Apple Silicon——Intel Mac 目前暂不支持。",
unavailable: "暂不可用",
},
cli: {

View File

@@ -7,8 +7,8 @@
* Known limitation: Safari on macOS always reports `Intel Mac OS X`
* in the UA string even on Apple Silicon, and Safari does not
* implement userAgentData. This function therefore returns `arm64`
* as the best default for any Mac — UI surfaces a small "On Intel
* Mac? Use CLI." hint to cover the Intel minority.
* as the best default for any Mac — UI surfaces a small hint that
* points Intel users to the architecture-specific download below.
*/
export type OSName = "mac" | "windows" | "linux" | "unknown";
@@ -85,7 +85,7 @@ export async function detectOS(): Promise<DetectResult> {
let arch: Arch = "unknown";
if (os === "mac") {
// Best default. Real Intel Mac users will see the disclaimer.
// Best default. Real Intel Mac users will see the architecture hint.
arch = "arm64";
} else if (/arm|aarch/i.test(ua)) {
arch = "arm64";

View File

@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import { parseReleaseAssets } from "./parse-release-assets";
function asset(name: string) {
return {
name,
browser_download_url: `https://github.test/releases/${name}`,
};
}
describe("parseReleaseAssets", () => {
it("keeps both Apple Silicon and Intel macOS installers", () => {
const assets = parseReleaseAssets([
asset("multica-desktop-0.4.2-mac-arm64.dmg"),
asset("multica-desktop-0.4.2-mac-arm64.zip"),
asset("multica-desktop-0.4.2-mac-x64.dmg"),
asset("multica-desktop-0.4.2-mac-x64.zip"),
asset("multica-desktop-0.4.2-mac-x64.dmg.blockmap"),
asset("latest-x64-mac.yml"),
]);
expect(assets).toEqual({
macArm64Dmg:
"https://github.test/releases/multica-desktop-0.4.2-mac-arm64.dmg",
macArm64Zip:
"https://github.test/releases/multica-desktop-0.4.2-mac-arm64.zip",
macX64Dmg:
"https://github.test/releases/multica-desktop-0.4.2-mac-x64.dmg",
macX64Zip:
"https://github.test/releases/multica-desktop-0.4.2-mac-x64.zip",
});
});
});

View File

@@ -21,6 +21,8 @@ export interface GitHubAsset {
export interface DownloadAssets {
macArm64Dmg?: string;
macArm64Zip?: string;
macX64Dmg?: string;
macX64Zip?: string;
winX64Exe?: string;
winArm64Exe?: string;
linuxAmd64AppImage?: string;
@@ -63,9 +65,13 @@ export function parseReleaseAssets(raw: GitHubAsset[]): DownloadAssets {
const url = asset.browser_download_url;
if (platform === "mac") {
if (archLower !== "arm64") continue; // we only ship arm64 today
if (extLower === "dmg") out.macArm64Dmg = url;
else if (extLower === "zip") out.macArm64Zip = url;
if (archLower === "arm64") {
if (extLower === "dmg") out.macArm64Dmg = url;
else if (extLower === "zip") out.macArm64Zip = url;
} else if (archLower === "x64") {
if (extLower === "dmg") out.macX64Dmg = url;
else if (extLower === "zip") out.macX64Zip = url;
}
} else if (platform === "windows") {
if (extLower !== "exe") continue;
if (archLower === "x64") out.winX64Exe = url;