From 8fc26cc44fbaee10f17e1e14946c34ef07228819 Mon Sep 17 00:00:00 2001 From: Yaowei Zheng Date: Thu, 6 Aug 2026 20:08:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(landing):=20desktop-first=20homepage=20?= =?UTF-8?q?=E2=80=94=20download=20CTA=20leads,=20CLI=20below=20the=20fold?= =?UTF-8?q?=20(#221)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude Fable 5 --- packages/landing/src/lib/platform.ts | 15 ++++ packages/landing/src/lib/strings-en.ts | 14 ++- packages/landing/src/lib/strings.ts | 17 ++-- packages/landing/src/pages/download.tsx | 12 +-- packages/landing/src/sections/cta.tsx | 20 +++-- packages/landing/src/sections/hero.tsx | 93 +++++++------------- packages/landing/src/sections/quickstart.tsx | 5 +- 7 files changed, 86 insertions(+), 90 deletions(-) create mode 100644 packages/landing/src/lib/platform.ts diff --git a/packages/landing/src/lib/platform.ts b/packages/landing/src/lib/platform.ts new file mode 100644 index 0000000..6eade95 --- /dev/null +++ b/packages/landing/src/lib/platform.ts @@ -0,0 +1,15 @@ +/** + * Coarse client-side OS detection, shared by the hero's platform-aware download + * button and the download page's platform cards. Detection only tunes emphasis + * (which label the hero button carries, which card gets badged) — absent or wrong + * detection never blocks a download. + */ +export type Platform = "mac" | "windows" | "linux"; + +export function detectPlatform(): Platform | null { + const ua = navigator.userAgent; + if (/Windows/i.test(ua)) return "windows"; + if (/Macintosh|Mac OS X/i.test(ua)) return "mac"; + if (/Linux|X11/.test(ua) && !/Android/i.test(ua)) return "linux"; + return null; +} diff --git a/packages/landing/src/lib/strings-en.ts b/packages/landing/src/lib/strings-en.ts index a30fadd..53de34d 100644 --- a/packages/landing/src/lib/strings-en.ts +++ b/packages/landing/src/lib/strings-en.ts @@ -54,9 +54,14 @@ export const en: Strings = { titleWords: ["Desktop", "Server"], titleSuffix: "", subtitle: "Create Self-Evolving Agents in One Click", - ctaPrimary: "Get started", + /** Primary CTA: base label, and the platform-aware variant once the OS is detected. */ + downloadCta: "Download the desktop app", + downloadCtaFor: (platform: string) => `Download for ${platform}`, + ctaQuickstart: "Get started", ctaGithub: "GitHub", - installHint: "One-line install (bundled Node runtime — unpack and run)", + /** Line under the CTAs: all installers on /download, CLI / self-hosted below the fold. */ + downloadAll: "All platforms (macOS / Windows / Linux)", + cliAlt: "CLI and self-hosted install ↓", stats: [ { value: "1000+", label: "supported models" }, { value: "1×CPU", label: "minimum footprint" }, @@ -80,7 +85,7 @@ export const en: Strings = { }, offlineRelease: "Download offline packages from GitHub Releases", desktopNote: - "Prefer a standalone app? The desktop app packages the Web interface and the server into a terminal-free installer (macOS / Windows / Linux), sharing the same local data as a CLI install.", + "Already on the desktop app? A CLI install shares the same local data with it, and the two can be used side by side.", desktopPage: "Go to the desktop download page", }, @@ -485,7 +490,8 @@ export const en: Strings = { title: "Complex AI development, made ever simpler", subtitle: "Through continuous evolution, PenguinHarness gives you a more efficient, more reliable, lower-hallucination and lower-cost Agent productivity engine.", - install: "Install now", + download: "Download the desktop app", + quickstart: "Get started", docs: "Read the docs", }, diff --git a/packages/landing/src/lib/strings.ts b/packages/landing/src/lib/strings.ts index 07fc4ce..786adb9 100644 --- a/packages/landing/src/lib/strings.ts +++ b/packages/landing/src/lib/strings.ts @@ -59,9 +59,14 @@ export const zh = { titleWords: ["桌面", "服务器"], titleSuffix: "上", subtitle: "一键创建自进化 Agent", - ctaPrimary: "快速开始", + /** Primary CTA: base label, and the platform-aware variant once the OS is detected. */ + downloadCta: "下载桌面版", + downloadCtaFor: (platform: string) => `下载桌面版(${platform})`, + ctaQuickstart: "快速开始", ctaGithub: "GitHub", - installHint: "一行命令安装(内嵌 Node 运行时,解压即用)", + /** Line under the CTAs: all installers on /download, CLI / self-hosted below the fold. */ + downloadAll: "全部平台下载(macOS / Windows / Linux)", + cliAlt: "命令行与自托管安装 ↓", stats: [ { value: "1000+", label: "支持模型数量" }, { value: "1×CPU", label: "最低运行配置" }, @@ -70,7 +75,7 @@ export const zh = { ], }, - /** Install-method switcher (hero + quick start): OS tabs, online/offline methods. */ + /** Install-method switcher (quick start step 1): OS tabs, online/offline methods. */ install: { linux: "Linux", macos: "macOS", @@ -85,8 +90,7 @@ export const zh = { windows: "解压后也可直接双击 install.cmd 完成安装。", }, offlineRelease: "前往 GitHub Releases 下载离线安装包", - desktopNote: - "更喜欢独立应用?桌面端把 Web 界面与服务端打包成免终端的安装程序(macOS / Windows / Linux),与 CLI 安装共用同一份本地数据。", + desktopNote: "已经装了桌面版?CLI 安装与桌面版共用同一份本地数据,两者可以随时并用。", desktopPage: "前往桌面端下载页", }, @@ -475,7 +479,8 @@ export const zh = { title: "让复杂的 AI 开发越来越简单", subtitle: "通过不断进化,PenguinHarness 为你提供更高效、更可靠、更低幻觉、更低成本的 Agent 生产力引擎。", - install: "立即安装", + download: "下载桌面版", + quickstart: "快速开始", docs: "阅读文档", }, diff --git a/packages/landing/src/pages/download.tsx b/packages/landing/src/pages/download.tsx index 1ec67cd..f5a7da9 100644 --- a/packages/landing/src/pages/download.tsx +++ b/packages/landing/src/pages/download.tsx @@ -23,22 +23,14 @@ import { OSS_ORIGIN, RELEASES_URL, } from "../lib/links"; +import { detectPlatform } from "../lib/platform"; +import type { Platform } from "../lib/platform"; import { Section } from "../components/section"; import { CodeCard } from "../components/code-card"; import { ChevronDownIcon, DownloadIcon, ExternalLinkIcon } from "../components/icons"; -type Platform = "mac" | "windows" | "linux"; const PLATFORMS: Platform[] = ["mac", "windows", "linux"]; -/** Coarse OS detection, used only to badge one card; absent or wrong detection changes nothing else. */ -function detectPlatform(): Platform | null { - const ua = navigator.userAgent; - if (/Windows/i.test(ua)) return "windows"; - if (/Macintosh|Mac OS X/i.test(ua)) return "mac"; - if (/Linux|X11/.test(ua) && !/Android/i.test(ua)) return "linux"; - return null; -} - interface Mirror { tag: string; base: string; diff --git a/packages/landing/src/sections/cta.tsx b/packages/landing/src/sections/cta.tsx index f8ec440..2c86b5f 100644 --- a/packages/landing/src/sections/cta.tsx +++ b/packages/landing/src/sections/cta.tsx @@ -1,11 +1,13 @@ -/** Closing call-to-action: the productivity-engine pitch + install / docs buttons. */ +/** Closing call-to-action: the productivity-engine pitch + download / quick start / docs buttons. */ import { Link } from "react-router"; import { S } from "../lib/strings"; import { DOCS_URL } from "../lib/links"; import { Section } from "../components/section"; -import { ArrowRightIcon } from "../components/icons"; +import { DownloadIcon } from "../components/icons"; export function Cta() { + const secondary = + "inline-flex h-11 items-center rounded-lg border border-gray-200 bg-white px-5 text-sm font-medium text-gray-900 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:bg-gray-800"; return (
@@ -17,16 +19,16 @@ export function Cta() {

- {S.cta.install} - + + {S.cta.download} - + + {S.cta.quickstart} + + {S.cta.docs}
diff --git a/packages/landing/src/sections/hero.tsx b/packages/landing/src/sections/hero.tsx index 88fd6cf..b8b4c84 100644 --- a/packages/landing/src/sections/hero.tsx +++ b/packages/landing/src/sections/hero.tsx @@ -1,66 +1,20 @@ /** * Hero: enlarged logo + product name, the one-line headline whose rotating word * crossfades through a gaussian blur (Desktop <-> Server, localized per dictionary), - * the one-sentence subtitle, the install one-liner behind an OS switcher, and stats. + * the desktop-first CTAs — a platform-aware download button pointing at /download + * (artifact resolution stays on that page), an all-platforms line and a link down to + * the quick start for CLI / self-hosted installs — and stats. * The rotating word is a stacked inline-grid so line width never jumps. */ import { useEffect, useState } from "react"; import { Link } from "react-router"; import { S } from "../lib/strings"; -import { INSTALL_CMD, INSTALL_CMD_WINDOWS, REPO_URL } from "../lib/links"; -import { CopyButton } from "../components/copy-button"; -import { ArrowRightIcon, GitHubIcon } from "../components/icons"; +import { REPO_URL } from "../lib/links"; +import { detectPlatform } from "../lib/platform"; +import { ArrowRightIcon, DownloadIcon, GitHubIcon } from "../components/icons"; const ROTATE_MS = 2600; -type InstallOs = "linux" | "macos" | "windows"; - -/** - * Install one-liner behind an OS tab row: one command visible at a time (Linux and - * macOS share the POSIX one-liner; the tabs still name them apart so nobody has to - * know that). Prompt char follows the shell: $ for POSIX, > for PowerShell. - */ -function InstallBox() { - const [os, setOs] = useState("linux"); - const cmd = os === "windows" ? INSTALL_CMD_WINDOWS : INSTALL_CMD; - const osBtn = (active: boolean) => - `rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${ - active - ? "bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-gray-100" - : "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" - }`; - return ( -
-
- {(["linux", "macos", "windows"] as const).map((key) => ( - - ))} -
-
- - - {os === "windows" ? ">" : "$"} - - {cmd} - - -
-
- ); -} - function RotatingWord({ words }: { words: string[] }) { const [active, setActive] = useState(0); useEffect(() => { @@ -86,6 +40,15 @@ function RotatingWord({ words }: { words: string[] }) { } export function Hero() { + // Platform-aware label only; every link goes to /download, where the platform + // cards and the GitHub/OSS artifact resolution live. + const detected = detectPlatform(); + const downloadLabel = detected + ? S.hero.downloadCtaFor(S.download.platforms[detected].name) + : S.hero.downloadCta; + const textLink = + "inline-flex items-center gap-1 text-brand-700 underline decoration-brand-300 underline-offset-2 transition-colors hover:text-brand-600 dark:text-brand-300 dark:decoration-brand-700"; + return (
-
- -

{S.hero.installHint}

-
+ + {S.hero.downloadAll} + + · + + {S.hero.cliAlt} + +

)} - {/* Below both install methods: the desktop app replaces this whole flow for - users who never want a terminal, so it stays visible on every switch. */} + {/* Below both install methods: the desktop app is the headline install path + (hero + /download); this note tells CLI installers the two share one data + root, and still routes anyone terminal-averse back to the download page. */}

{S.install.desktopNote}{" "}