mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-25 03:55:27 +02:00
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:
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
29
apps/web/features/landing/components/download/hero.test.ts
Normal file
29
apps/web/features/landing/components/download/hero.test.ts
Normal 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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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";
|
||||
|
||||
33
apps/web/features/landing/utils/parse-release-assets.test.ts
Normal file
33
apps/web/features/landing/utils/parse-release-assets.test.ts
Normal 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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user