merge: bring stripe pricing console into release v0.2

This commit is contained in:
Haitao Pan 2026-03-17 19:29:51 +08:00
commit 67a119631d
4 changed files with 255 additions and 153 deletions

View File

@ -8,6 +8,8 @@
:root {
--font-geist-sans: "Geist", sans-serif;
--font-geist-mono: "Geist Mono", monospace;
--font-editorial-display:
"Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif;
--app-shell-nav-offset: 5.5rem;
/* Light theme defaults */
@ -89,6 +91,11 @@ body {
font-size: var(--type-body-size);
line-height: var(--type-body-line-height);
background-color: var(--color-background);
background-image: linear-gradient(
180deg,
rgba(255, 255, 255, 0.58),
rgba(255, 255, 255, 0)
);
color: var(--color-text);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
@ -149,6 +156,13 @@ button {
}
}
@layer components {
.editorial-display {
font-family: var(--font-editorial-display);
letter-spacing: -0.04em;
}
}
@media (max-width: 1023px) {
.mobile-home-shell {
--color-background: #fbfaf7;

View File

@ -18,19 +18,25 @@ import {
Terminal,
Users,
} from "lucide-react";
import useSWR from "swr";
import Footer from "../components/Footer";
import { HeroCard } from "../components/HeroCard";
import UnifiedNavigation from "../components/UnifiedNavigation";
import { useUserStore } from "../lib/userStore";
import { useLanguage } from "../i18n/LanguageProvider";
import { translations } from "../i18n/translations";
import { useMoltbotStore } from "../lib/moltbotStore";
import { useUserStore } from "../lib/userStore";
import { cn } from "../lib/utils";
import { AskAIDialog } from "../components/AskAIDialog";
import { HeroCard } from "../components/HeroCard";
import useSWR from "swr";
const HOME_SECTION_CLASS =
"rounded-[2rem] border border-slate-900/10 bg-white/90 shadow-[0_18px_40px_rgba(15,23,42,0.05)]";
const HOME_SECTION_LABEL_CLASS =
"text-[0.68rem] font-semibold uppercase tracking-[0.26em] text-text-subtle";
const HOME_LIST_CARD_CLASS =
"rounded-[1.5rem] border border-slate-900/10 bg-[#fcfbf8] transition duration-200";
const iconMap: Record<string, any> = {
// English keys
"Global Acceleration Network": Link,
"Full-link SaaS Hosting": Layers,
"AI-Driven Observability": Sparkles,
@ -45,7 +51,6 @@ const iconMap: Record<string, any> = {
"Machine-to-Machine": Layers,
"Connect via CLI": Terminal,
"REST & Admin APIs": Link,
// Chinese keys
全球加速网络: Link,
"全链路 SaaS 托管": Layers,
"AI 驱动的可观测性": Sparkles,
@ -67,22 +72,22 @@ export default function HomePage() {
const { mode, isOpen } = useMoltbotStore();
return (
<div className="mobile-home-shell min-h-screen bg-background text-text transition-colors duration-150 flex flex-col overflow-x-hidden">
<div className="mobile-home-shell relative flex min-h-screen flex-col overflow-x-hidden bg-background text-text transition-colors duration-150">
<div
aria-hidden
className="pointer-events-none absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.56),rgba(255,255,255,0))]"
/>
<UnifiedNavigation />
<div
className={cn(
"flex flex-1 relative overflow-hidden",
"relative flex flex-1 overflow-hidden",
mode === "left-sidebar" && isOpen && "flex-row-reverse",
)}
>
<div className="flex-1 overflow-y-auto relative">
<div className="relative flex-1 overflow-y-auto">
<div className="relative mx-auto max-w-6xl px-4 pb-16 sm:px-6 sm:pb-20">
<div
className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(37,78,219,0.08),transparent_28%),radial-gradient(circle_at_bottom_right,rgba(15,23,42,0.05),transparent_32%),linear-gradient(180deg,rgba(255,255,255,0.82),transparent_58%)]"
aria-hidden
/>
<main className="relative space-y-8 pt-6 sm:space-y-12 sm:pt-10">
<main className="relative space-y-6 pt-6 sm:space-y-8 sm:pt-10">
<HeroSection />
<NextStepsSection />
<StatsSection />
@ -101,70 +106,113 @@ export default function HomePage() {
export function HeroSection() {
const { user } = useUserStore();
const { language } = useLanguage();
const isChinese = language === "zh";
const t = translations[language].marketing.home;
return (
<section className="grid gap-8 lg:grid-cols-[0.9fr_1.1fr] lg:gap-12">
<div className="flex flex-col justify-center space-y-6 sm:space-y-8">
<div className="space-y-3 sm:space-y-4">
{t.hero.eyebrow && (
<p className="font-semibold uppercase tracking-[0.28em] text-text-subtle">
{t.hero.eyebrow}
<section className="relative overflow-hidden rounded-[2.75rem] border border-slate-900/10 bg-[linear-gradient(180deg,#ffffff,#faf7f2)] p-6 shadow-[0_24px_56px_rgba(15,23,42,0.05)] sm:p-8 lg:p-10">
<div aria-hidden className="pointer-events-none absolute inset-0">
<div className="absolute left-[8%] top-[8%] h-[16rem] w-[16rem] rounded-full bg-[radial-gradient(circle,rgba(37,78,219,0.1),transparent_64%)] blur-3xl" />
<div className="absolute left-[30%] top-[12%] h-[14rem] w-[14rem] rounded-full bg-[radial-gradient(circle,rgba(245,211,170,0.42),transparent_66%)] blur-3xl" />
<div className="absolute right-[10%] top-[10%] h-[18rem] w-[18rem] rounded-full bg-[radial-gradient(circle,rgba(255,255,255,0.92),transparent_72%)]" />
<div className="absolute inset-x-0 top-0 h-[18rem] bg-[linear-gradient(180deg,rgba(255,255,255,0.78),rgba(255,255,255,0)_72%)]" />
</div>
<div className="relative grid gap-8 lg:grid-cols-[0.96fr_1.04fr] lg:gap-12">
<div className="flex flex-col justify-between gap-8">
<div className="space-y-5">
{t.hero.eyebrow ? (
<p className={HOME_SECTION_LABEL_CLASS}>{t.hero.eyebrow}</p>
) : null}
<h1
className={cn(
"max-w-[11ch] leading-[0.88] text-heading",
isChinese
? "text-[2.85rem] font-semibold tracking-[-0.08em] sm:text-[3.4rem] lg:text-[4.5rem]"
: "editorial-display text-[3.05rem] tracking-[-0.06em] sm:text-[3.6rem] lg:text-[4.8rem]",
)}
>
{t.hero.title}
</h1>
<p className="max-w-xl text-[1rem] leading-8 text-text-muted sm:text-[1.05rem]">
{t.hero.subtitle}
</p>
)}
<h1 className="max-w-[12ch] text-[2.22rem] font-semibold leading-[0.92] tracking-[-0.075em] text-heading sm:max-w-none sm:text-3xl lg:text-[3.35rem]">
{t.hero.title}
</h1>
<p className="max-w-xl text-[1.02rem] leading-7 text-text-muted">
{t.hero.subtitle}
</p>
</div>
<div className="grid gap-2 sm:flex sm:flex-wrap sm:items-center sm:gap-3">
{user ? (
<div className="flex items-center justify-center gap-2 rounded-full border border-success/30 bg-success/10 px-4 py-2 text-sm font-medium text-success sm:justify-start sm:py-1.5">
<div className="h-2 w-2 rounded-full bg-success animate-pulse" />
{t.signedIn.replace("{{username}}", user.username)}
</div>
) : (
<button className="flex items-center justify-center gap-2 rounded-full bg-primary px-6 py-3 text-sm font-semibold text-white transition hover:bg-primary-hover sm:py-2.5">
<PlusCircle className="h-4 w-4" />
{t.heroButtons.create}
</div>
<div className="flex flex-wrap gap-3">
{user ? (
<div className="inline-flex items-center gap-2 rounded-full border border-success/25 bg-success/10 px-4 py-2 text-sm font-semibold text-success">
<div className="h-2 w-2 rounded-full bg-success animate-pulse" />
{t.signedIn.replace("{{username}}", user.username)}
</div>
) : (
<button
type="button"
className="inline-flex items-center gap-2 rounded-full bg-slate-950 px-6 py-3 text-sm font-semibold text-white transition hover:bg-primary"
>
<PlusCircle className="h-4 w-4" />
{t.heroButtons.create}
</button>
)}
<button
type="button"
className="inline-flex items-center gap-2 rounded-full border border-slate-900/10 bg-white px-6 py-3 text-sm font-semibold text-slate-800 transition hover:bg-slate-50"
>
<Play className="h-4 w-4" />
{t.heroButtons.playground}
</button>
)}
<button className="flex items-center justify-center gap-2 rounded-full border border-surface-border bg-surface/90 px-6 py-3 text-sm font-semibold text-text transition hover:bg-surface-hover sm:py-2.5">
<Play className="h-4 w-4" />
{t.heroButtons.playground}
</button>
<button className="flex items-center justify-center gap-2 rounded-full border border-surface-border bg-surface/90 px-6 py-3 text-sm font-semibold text-text transition hover:bg-surface-hover sm:py-2.5">
<BookOpen className="h-4 w-4" />
{t.heroButtons.tutorials}
</button>
</div>
<div className="flex flex-col gap-3 text-sm">
<p className="text-text-muted">{t.trustedBy}</p>
<div className="flex flex-wrap gap-2">
<LogoPill label="Next.js" />
<LogoPill label="Go" />
<LogoPill label="Vercel" />
<LogoPill label="Cloud Run" />
<LogoPill label="PostgreSQL" />
<button
type="button"
className="inline-flex items-center gap-2 rounded-full border border-slate-900/10 bg-[#f8f4ec] px-6 py-3 text-sm font-semibold text-slate-800 transition hover:bg-[#f2ebdd]"
>
<BookOpen className="h-4 w-4" />
{t.heroButtons.tutorials}
</button>
</div>
<div className="space-y-3 border-t border-slate-900/10 pt-5">
<p className={HOME_SECTION_LABEL_CLASS}>{t.trustedBy}</p>
<div className="flex flex-wrap gap-2">
<LogoPill label="Next.js" />
<LogoPill label="Go" />
<LogoPill label="Vercel" />
<LogoPill label="Cloud Run" />
<LogoPill label="PostgreSQL" />
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-3 sm:gap-4">
<div className="relative flex flex-col gap-3 sm:gap-4">
{t.heroCards.map((card) => {
const Icon = getIcon(card.title, PlusCircle);
return (
<HeroCard
key={card.title}
icon={Icon}
title={card.title}
description={card.description}
guide={card.guide}
/>
);
})}
<div className="flex flex-col gap-3 lg:pl-4">
<div className="flex flex-col gap-2 border-b border-slate-900/10 pb-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className={HOME_SECTION_LABEL_CLASS}>
{isChinese ? "主要入口" : "Launch paths"}
</p>
<p className="mt-2 max-w-md text-sm leading-6 text-text-muted">
{isChinese
? "从接入、托管到观测,保留原有入口,但改成更轻的阅读节奏。"
: "Keep the same entry points, but present them with a calmer editorial rhythm."}
</p>
</div>
<span className="hidden rounded-full border border-slate-900/10 bg-white px-3 py-1 text-xs font-semibold text-slate-600 sm:inline-flex">
{t.heroCards.length} {isChinese ? "个入口" : "entry paths"}
</span>
</div>
<div className="space-y-3">
{t.heroCards.map((card) => {
const Icon = getIcon(card.title, PlusCircle);
return (
<HeroCard
key={card.title}
icon={Icon}
title={card.title}
description={card.description}
guide={card.guide}
/>
);
})}
</div>
</div>
</div>
</section>
@ -176,40 +224,50 @@ export function NextStepsSection() {
const t = translations[language].marketing.home;
return (
<section className="space-y-4 rounded-[1.75rem] border border-surface-border/70 bg-white/70 p-5 shadow-[0_16px_45px_rgba(15,23,42,0.05)] lg:rounded-none lg:border-transparent lg:bg-transparent lg:p-0 lg:shadow-none">
<header className="flex flex-col gap-2 text-sm text-text-muted sm:flex-row sm:items-center sm:gap-3">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-text-subtle">
{t.nextSteps.title}
</p>
<span className="w-fit rounded-full bg-surface-muted px-3 py-1 text-xs font-semibold text-primary">
<section className={cn(HOME_SECTION_CLASS, "space-y-4 p-5 lg:p-7")}>
<header className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className={HOME_SECTION_LABEL_CLASS}>{t.nextSteps.title}</p>
<p className="mt-2 text-sm leading-6 text-text-muted">
{language === "zh"
? "保留原有 onboarding 内容,但改成更轻、更整齐的起步列表。"
: "Keep the same onboarding content, in a lighter and calmer starting list."}
</p>
</div>
<span className="inline-flex w-fit items-center rounded-full border border-slate-900/10 bg-[#f8f4ec] px-3 py-1 text-xs font-semibold text-slate-700">
{t.nextSteps.badge}
</span>
</header>
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-4">
{t.nextSteps.items.map((item, index: number) => {
const Icon = getIcon(item.title, Users);
return (
<div
key={index}
className="flex items-start gap-3 rounded-[1.4rem] border border-surface-border bg-surface/92 p-4 shadow-lg shadow-shadow-sm"
className={cn(
HOME_LIST_CARD_CLASS,
"flex h-full flex-col justify-between gap-6 p-4 hover:-translate-y-[1px] hover:bg-white",
)}
>
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-primary/12 text-primary">
<Icon className="h-5 w-5" aria-hidden />
</div>
<div className="space-y-2">
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wide text-primary-muted">
<span className="rounded-full bg-primary/20 px-2 py-0.5">
<div className="space-y-4">
<div className="flex items-center justify-between gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-slate-900/[0.04] text-primary">
<Icon className="h-5 w-5" aria-hidden />
</div>
<span className="rounded-full border border-slate-900/10 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">
{item.status}
</span>
</div>
<p className="text-sm font-semibold text-heading">
<p className="text-base font-semibold leading-7 text-slate-900">
{item.title}
</p>
<button className="inline-flex items-center gap-1 text-xs font-semibold text-primary transition hover:text-primary-hover">
{t.nextSteps.learnMore}
<ArrowRight className="h-4 w-4" aria-hidden />
</button>
</div>
<button className="inline-flex items-center gap-1 text-sm font-semibold text-primary transition hover:text-primary-hover">
{t.nextSteps.learnMore}
<ArrowRight className="h-4 w-4" aria-hidden />
</button>
</div>
);
})}
@ -286,17 +344,33 @@ export function StatsSection() {
];
return (
<section className="overflow-hidden rounded-[1.9rem] border border-surface-border/70 bg-[linear-gradient(135deg,rgba(255,255,255,0.92),rgba(243,244,246,0.88))] p-5 shadow-[0_18px_40px_rgba(15,23,42,0.05)] sm:p-6">
<div className="grid grid-cols-2 gap-x-4 gap-y-6 md:grid-cols-3 lg:grid-cols-5">
<section className={cn(HOME_SECTION_CLASS, "space-y-5 p-5 lg:p-7")}>
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className={HOME_SECTION_LABEL_CLASS}>
{language === "zh" ? "平台统计" : "Platform pulse"}
</p>
<p className="mt-2 text-sm leading-6 text-text-muted">
{language === "zh"
? "把关键数字收在同一条平静的视线上,不再单独做成重型数据舱。"
: "Keep key numbers in the same calm visual rhythm instead of a separate heavy dashboard block."}
</p>
</div>
<span className="inline-flex w-fit items-center rounded-full border border-slate-900/10 bg-white px-3 py-1 text-xs font-semibold text-slate-600">
{language === "zh" ? "每小时更新" : "Updated hourly"}
</span>
</div>
<div className="grid gap-3 md:grid-cols-3 lg:grid-cols-5">
{displayStats.map((stat, index: number) => (
<div
key={index}
className="space-y-1 text-left even:text-right md:text-left"
className="rounded-[1.5rem] border border-slate-900/10 bg-[#fcfbf8] px-4 py-5"
>
<div className="text-[2rem] font-semibold tracking-[-0.06em] text-heading sm:text-3xl">
<div className="editorial-display text-[2.2rem] leading-none text-slate-950 sm:text-[2.7rem]">
{stat.value}
</div>
<p className="max-w-[9rem] text-sm text-text-muted even:ml-auto md:max-w-none">
<p className="mt-3 text-sm leading-6 text-slate-600">
{stat.label}
</p>
</div>
@ -306,16 +380,6 @@ export function StatsSection() {
);
}
type HomeStatsResponse = {
registeredUsers: number | null;
visits: {
daily: number | null;
weekly: number | null;
monthly: number | null;
};
updatedAt: string;
};
export function ShortcutsSection() {
const { language } = useLanguage();
const t = translations[language].marketing.home;
@ -352,26 +416,37 @@ export function ShortcutsSection() {
}));
return (
<section className="space-y-4 rounded-[1.75rem] border border-surface-border/70 bg-white/70 p-5 shadow-[0_16px_45px_rgba(15,23,42,0.05)] lg:rounded-none lg:border-transparent lg:bg-transparent lg:p-0 lg:shadow-none">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<section className={cn(HOME_SECTION_CLASS, "space-y-4 p-5 lg:p-7")}>
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-text-subtle">
{t.shortcuts.title}
<p className={HOME_SECTION_LABEL_CLASS}>{t.shortcuts.title}</p>
<p className="mt-2 text-sm leading-6 text-text-muted">
{t.shortcuts.subtitle}
</p>
<p className="mt-1 text-sm text-text-muted">{t.shortcuts.subtitle}</p>
</div>
<div className="flex flex-wrap gap-2 text-xs font-semibold text-primary">
<button className="rounded-full border border-surface-border bg-surface-muted px-3 py-2 transition hover:bg-surface-hover">
<div className="flex flex-wrap gap-2 text-xs font-semibold">
<button
type="button"
className="rounded-full border border-slate-900/10 bg-slate-950 px-3 py-2 text-white transition hover:bg-primary"
>
{t.shortcuts.buttons.start}
</button>
<button className="rounded-full border border-surface-border bg-surface-muted px-3 py-2 transition hover:bg-surface-hover">
<button
type="button"
className="rounded-full border border-slate-900/10 bg-white px-3 py-2 text-slate-700 transition hover:bg-slate-50"
>
{t.shortcuts.buttons.docs}
</button>
<button className="rounded-full border border-surface-border bg-surface-muted px-3 py-2 transition hover:bg-surface-hover">
<button
type="button"
className="rounded-full border border-slate-900/10 bg-white px-3 py-2 text-slate-700 transition hover:bg-slate-50"
>
{t.shortcuts.buttons.guides}
</button>
</div>
</div>
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
{shortcutItems.map((item, index: number) => {
const Icon = getIcon(item.title, Sparkles);
@ -379,19 +454,24 @@ export function ShortcutsSection() {
<a
key={index}
href={item.href}
className="group flex items-start gap-3 rounded-[1.4rem] border border-surface-border bg-surface/92 p-4 transition hover:-translate-y-[1px] hover:border-primary/50 hover:bg-surface-hover"
className={cn(
HOME_LIST_CARD_CLASS,
"group flex items-start gap-3 p-4 hover:-translate-y-[1px] hover:bg-white",
)}
>
<div className="mt-1 flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-primary/12 text-primary">
<div className="mt-1 flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-slate-900/[0.04] text-primary">
<Icon className="h-5 w-5" aria-hidden />
</div>
<div className="min-w-0 space-y-1">
<div className="text-sm font-semibold text-heading">
<div className="text-base font-semibold leading-7 text-slate-900">
{item.title}
</div>
<p className="text-sm text-text-muted">{item.description}</p>
<p className="text-sm leading-6 text-slate-600">
{item.description}
</p>
</div>
<ArrowRight
className="ml-auto h-4 w-4 text-text-subtle transition group-hover:text-primary"
className="ml-auto h-4 w-4 text-slate-500 transition group-hover:text-primary"
aria-hidden
/>
</a>
@ -402,6 +482,16 @@ export function ShortcutsSection() {
);
}
type HomeStatsResponse = {
registeredUsers: number | null;
visits: {
daily: number | null;
weekly: number | null;
monthly: number | null;
};
updatedAt: string;
};
type LatestBlogPost = {
slug: string;
title: string;
@ -410,8 +500,8 @@ type LatestBlogPost = {
function LogoPill({ label }: { label: string }) {
return (
<span className="inline-flex items-center gap-2 rounded-full border border-surface-border bg-surface/88 px-3.5 py-1.5 text-xs font-semibold text-text shadow-[0_8px_22px_rgba(15,23,42,0.04)]">
<div className="h-2 w-2 rounded-full bg-success" />
<span className="inline-flex items-center gap-2 rounded-full border border-slate-900/10 bg-white px-3.5 py-1.5 text-xs font-semibold text-slate-700">
<div className="h-2 w-2 rounded-full bg-primary" />
{label}
</span>
);

View File

@ -36,25 +36,16 @@ export default function Footer() {
};
return (
<footer className="mt-12 flex flex-col items-center justify-center gap-4 rounded-[1.75rem] border border-surface-border bg-surface/88 px-6 py-4 text-sm text-text-muted shadow-[0_18px_40px_rgba(15,23,42,0.05)] lg:rounded-2xl lg:border-white/10 lg:bg-white/5 lg:text-slate-300 lg:shadow-none">
<footer className="mt-12 flex flex-col items-center justify-center gap-4 rounded-[2rem] border border-white/75 bg-[linear-gradient(135deg,rgba(15,23,42,0.95),rgba(30,41,59,0.92))] px-6 py-5 text-sm text-slate-300 shadow-[0_24px_60px_rgba(15,23,42,0.16)]">
<div className="flex w-full flex-col items-center gap-4 sm:flex-row sm:justify-between">
<div className="flex gap-4 order-2 sm:order-1">
<Link
href="/terms"
className="transition-colors hover:text-text lg:hover:text-white"
>
<Link href="/terms" className="transition-colors hover:text-white">
{isChinese ? "服务条款" : "Terms of Service"}
</Link>
<Link
href="/privacy"
className="transition-colors hover:text-text lg:hover:text-white"
>
<Link href="/privacy" className="transition-colors hover:text-white">
{isChinese ? "隐私政策" : "Privacy Policy"}
</Link>
<Link
href="/support"
className="transition-colors hover:text-text lg:hover:text-white"
>
<Link href="/support" className="transition-colors hover:text-white">
{isChinese ? "联系我们" : "Contact Us"}
</Link>
</div>
@ -64,7 +55,7 @@ export default function Footer() {
<a
key={label}
href={href}
className="flex h-9 w-9 items-center justify-center rounded-full border border-surface-border bg-surface-muted text-text transition hover:border-surface-border-strong hover:text-text lg:border-white/10 lg:bg-white/5 lg:text-white lg:hover:border-indigo-400/50 lg:hover:text-indigo-100"
className="flex h-9 w-9 items-center justify-center rounded-full border border-white/10 bg-white/5 text-white transition hover:border-indigo-400/50 hover:text-indigo-100"
>
<Icon className="h-4 w-4" aria-hidden />
<span className="sr-only">{label}</span>
@ -78,7 +69,7 @@ export default function Footer() {
onClick={handleViewToggle}
aria-label={viewToggleLabel}
title={viewToggleLabel}
className="group flex h-10 w-10 items-center justify-center rounded-full border border-surface-border bg-surface-muted text-text transition hover:border-surface-border-strong focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 lg:border-white/10 lg:bg-white/5 lg:text-white lg:hover:border-indigo-400/50"
className="group flex h-10 w-10 items-center justify-center rounded-full border border-white/10 bg-white/5 text-white transition hover:border-indigo-400/50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
>
<span className="material-symbols-outlined text-xl">
{view === "classic" ? "view_quilt" : "view_cozy"}
@ -90,21 +81,21 @@ export default function Footer() {
aria-pressed={isDark}
aria-label={toggleLabel}
title={toggleLabel}
className="group relative flex h-10 w-20 items-center rounded-full border border-surface-border bg-surface-muted px-2 text-text transition hover:border-surface-border-strong focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 lg:border-white/10 lg:bg-white/5 lg:text-white lg:hover:border-indigo-400/50"
className="group relative flex h-10 w-20 items-center rounded-full border border-white/10 bg-white/5 px-2 text-white transition hover:border-indigo-400/50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
>
<span className="relative z-10 flex w-full items-center justify-between text-text-subtle lg:text-slate-300">
<span className="relative z-10 flex w-full items-center justify-between text-slate-300">
<Moon
className={`h-4 w-4 transition-colors ${isDark ? "text-text" : "text-text-subtle lg:text-slate-500"}`}
className={`h-4 w-4 transition-colors ${isDark ? "text-white" : "text-slate-500"}`}
aria-hidden
/>
<Sun
className={`h-4 w-4 transition-colors ${isDark ? "text-text-subtle lg:text-slate-500" : "text-amber-500 lg:text-amber-300"}`}
className={`h-4 w-4 transition-colors ${isDark ? "text-slate-500" : "text-amber-300"}`}
aria-hidden
/>
</span>
<span
aria-hidden
className={`absolute inset-y-1 left-1 h-8 w-8 rounded-full bg-background shadow-sm transition-transform duration-300 ease-out lg:bg-white/90 ${isDark ? "translate-x-0" : "translate-x-10"}`}
className={`absolute inset-y-1 left-1 h-8 w-8 rounded-full bg-white shadow-sm transition-transform duration-300 ease-out ${isDark ? "translate-x-0" : "translate-x-10"}`}
/>
</button>
</div>

View File

@ -4,6 +4,7 @@ import { useState, type KeyboardEvent } from "react";
import Link from "next/link";
import { ArrowRight, QrCode, X } from "lucide-react";
import { useLanguage } from "../i18n/LanguageProvider";
import { cn } from "../lib/utils";
interface GuideStep {
@ -32,6 +33,13 @@ export function HeroCard({
}: HeroCardProps) {
const [showGuide, setShowGuide] = useState(false);
const hasGuide = Boolean(guide);
const { language } = useLanguage();
const isChinese = language === "zh";
const guideLabel = isChinese ? "查看向导" : "View guide";
const qrTitle = isChinese ? "VLESS 协议就绪" : "VLESS Protocol Ready";
const qrDescription = isChinese
? "在控制台中扫码,即可快速完成连接。"
: "Scan the QR code in the control panel to connect automatically.";
function openGuide(): void {
if (!hasGuide) {
@ -59,26 +67,26 @@ export function HeroCard({
onClick={hasGuide ? openGuide : undefined}
onKeyDown={handleCardKeyDown}
className={cn(
"group relative flex items-start gap-4 overflow-hidden rounded-[1.6rem] border border-surface-border bg-white/88 p-5 shadow-[0_18px_42px_rgba(15,23,42,0.05)] transition-all duration-300 sm:rounded-2xl sm:p-6",
"group relative flex items-start gap-4 overflow-hidden rounded-[1.5rem] border border-slate-900/10 bg-[#fcfbf8] p-5 transition-all duration-200 sm:p-6",
hasGuide
? "cursor-pointer hover:border-primary/50 hover:bg-surface-hover"
: "hover:border-primary/50 hover:bg-surface-hover",
showGuide ? "border-primary/50 shadow-lg" : "",
? "cursor-pointer hover:-translate-y-[1px] hover:border-primary/35 hover:bg-white"
: "hover:-translate-y-[1px] hover:border-primary/25 hover:bg-white",
showGuide ? "border-primary/40 bg-white" : "",
)}
>
<div className="mt-1 rounded-full border border-surface-border bg-surface-muted p-2.5 group-hover:border-primary/50 group-hover:text-primary">
<div className="mt-1 rounded-full border border-slate-900/10 bg-white p-2.5 text-slate-700 group-hover:border-primary/30 group-hover:text-primary">
<Icon className="h-5 w-5" />
</div>
<div className="flex w-full flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4">
<div className="space-y-1">
<h3 className="text-base font-semibold tracking-[-0.03em] text-heading">
<div className="space-y-2">
<h3 className="text-[1.05rem] font-semibold leading-7 tracking-[-0.03em] text-slate-900">
{title}
</h3>
<p className="text-sm leading-6 text-text-muted">{description}</p>
<p className="text-sm leading-6 text-slate-600">{description}</p>
</div>
{hasGuide ? (
<span className="inline-flex w-fit shrink-0 items-center gap-1 rounded-full border border-primary/20 bg-primary/10 px-3 py-1.5 text-xs font-semibold text-primary">
<span className="inline-flex w-fit shrink-0 items-center gap-1 rounded-full border border-slate-900/10 bg-white px-3 py-1.5 text-xs font-semibold text-slate-600">
{guideLabel}
<ArrowRight className="h-3.5 w-3.5" />
</span>
) : null}
@ -153,11 +161,10 @@ export function HeroCard({
</div>
<div className="space-y-1">
<p className="text-sm font-semibold text-heading">
VLESS Protocol Ready
{qrTitle}
</p>
<p className="text-xs leading-relaxed text-text-muted">
Scan the QR code in the control panel to connect
automatically.
{qrDescription}
</p>
</div>
</div>