feat: 五站五种登录页设计(去 shadcn 默认味)+ 语言路由确认
- FormShell 重写为六种布局变体,五站各用一种:
· AI Stack = glass 深蓝渐变全屏 + 玻璃拟态浮动卡片
· LaunchAI = gradient 橙色渐变全屏 + 白色大圆角卡片
· The AI Ledger = editorial 报纸头版:双细线报头(日期/品牌/标语衬线居中)
+ 下划线输入 + 方角大写 tracking 黑按钮
· DevAI.tools = terminal 深色终端窗口(三色标题栏 + $ devai.tools auth --login)
+ 等宽输入 + 绿底黑字命令按钮
· findai = minimal 纯白居中 + 大圆角浅灰输入 + 紫色按钮
- 输入框/标签/社交按钮按 data-theme 形态变化(auth 路由注入 data-theme)
- 修复 FormShell 各变体根元素缺 w-full(被 auth flex 容器挤压成 1/3 宽)
- 回归:五站构建 OK / mock 登录 OK
This commit is contained in:
@@ -23,6 +23,7 @@ const SITES_ROOT = path.join(REPO_ROOT, "sites");
|
|||||||
// ====================================================================
|
// ====================================================================
|
||||||
const SITES = {
|
const SITES = {
|
||||||
"ai-stack": {
|
"ai-stack": {
|
||||||
|
authVariant: "glass",
|
||||||
themeClass: "site-ai-stack",
|
themeClass: "site-ai-stack",
|
||||||
brandZh: {
|
brandZh: {
|
||||||
name: "AI Stack",
|
name: "AI Stack",
|
||||||
@@ -92,6 +93,7 @@ const SITES = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
launchai: {
|
launchai: {
|
||||||
|
authVariant: "gradient",
|
||||||
themeClass: "site-launchai",
|
themeClass: "site-launchai",
|
||||||
brandZh: {
|
brandZh: {
|
||||||
name: "LaunchAI",
|
name: "LaunchAI",
|
||||||
@@ -159,6 +161,7 @@ const SITES = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"the-ai-ledger": {
|
"the-ai-ledger": {
|
||||||
|
authVariant: "editorial",
|
||||||
themeClass: "site-the-ai-ledger",
|
themeClass: "site-the-ai-ledger",
|
||||||
brandZh: {
|
brandZh: {
|
||||||
name: "The AI Ledger",
|
name: "The AI Ledger",
|
||||||
@@ -227,6 +230,7 @@ const SITES = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"devai-tools": {
|
"devai-tools": {
|
||||||
|
authVariant: "terminal",
|
||||||
themeClass: "site-devai-tools",
|
themeClass: "site-devai-tools",
|
||||||
brandZh: {
|
brandZh: {
|
||||||
name: "DevAI.tools",
|
name: "DevAI.tools",
|
||||||
@@ -293,6 +297,7 @@ const SITES = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findai: {
|
findai: {
|
||||||
|
authVariant: "minimal",
|
||||||
themeClass: "site-findai",
|
themeClass: "site-findai",
|
||||||
brandZh: {
|
brandZh: {
|
||||||
name: "findai",
|
name: "findai",
|
||||||
@@ -776,6 +781,7 @@ NEXT_PUBLIC_APP_URL="http://localhost:${site.port}"
|
|||||||
NEXT_PUBLIC_APP_NAME="${site.brand.name}"
|
NEXT_PUBLIC_APP_NAME="${site.brand.name}"
|
||||||
# 登录页品牌模板(分栏侧栏颜色与标语)
|
# 登录页品牌模板(分栏侧栏颜色与标语)
|
||||||
NEXT_PUBLIC_AUTH_ACCENT="${site.accent}"
|
NEXT_PUBLIC_AUTH_ACCENT="${site.accent}"
|
||||||
|
NEXT_PUBLIC_AUTH_VARIANT="${site.authVariant}"
|
||||||
NEXT_PUBLIC_AUTH_BRAND_TAGLINE="${site.brand.tagline}"
|
NEXT_PUBLIC_AUTH_BRAND_TAGLINE="${site.brand.tagline}"
|
||||||
# 开发阶段免邮箱验证(生产置 false 发送真实验证邮件)
|
# 开发阶段免邮箱验证(生产置 false 发送真实验证邮件)
|
||||||
DEV_SKIP_EMAIL_VERIFY="true"
|
DEV_SKIP_EMAIL_VERIFY="true"
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export function LoginForm() {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
variant={(process.env.NEXT_PUBLIC_AUTH_VARIANT as "glass" | "gradient" | "editorial" | "terminal" | "minimal" | "split" | undefined) ?? "split"}
|
||||||
brandName={process.env.NEXT_PUBLIC_APP_NAME}
|
brandName={process.env.NEXT_PUBLIC_APP_NAME}
|
||||||
brandTagline={process.env.NEXT_PUBLIC_AUTH_BRAND_TAGLINE}
|
brandTagline={process.env.NEXT_PUBLIC_AUTH_BRAND_TAGLINE}
|
||||||
accent={process.env.NEXT_PUBLIC_AUTH_ACCENT}
|
accent={process.env.NEXT_PUBLIC_AUTH_ACCENT}
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ export function SignupForm() {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
variant={(process.env.NEXT_PUBLIC_AUTH_VARIANT as "glass" | "gradient" | "editorial" | "terminal" | "minimal" | "split" | undefined) ?? "split"}
|
||||||
brandName={process.env.NEXT_PUBLIC_APP_NAME}
|
brandName={process.env.NEXT_PUBLIC_APP_NAME}
|
||||||
brandTagline={process.env.NEXT_PUBLIC_AUTH_BRAND_TAGLINE}
|
brandTagline={process.env.NEXT_PUBLIC_AUTH_BRAND_TAGLINE}
|
||||||
accent={process.env.NEXT_PUBLIC_AUTH_ACCENT}
|
accent={process.env.NEXT_PUBLIC_AUTH_ACCENT}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useTranslations } from 'next-intl';
|
|||||||
|
|
||||||
import { Form } from "@/components/ui/form";
|
import { Form } from "@/components/ui/form";
|
||||||
import { Logo } from "@/components/Logo";
|
import { Logo } from "@/components/Logo";
|
||||||
import { Button } from "@/components/button";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface FormShellProps<TFieldValues extends FieldValues> {
|
interface FormShellProps<TFieldValues extends FieldValues> {
|
||||||
@@ -23,14 +22,26 @@ interface FormShellProps<TFieldValues extends FieldValues> {
|
|||||||
className?: string;
|
className?: string;
|
||||||
headerSlot?: React.ReactNode;
|
headerSlot?: React.ReactNode;
|
||||||
socialSlot?: React.ReactNode;
|
socialSlot?: React.ReactNode;
|
||||||
/** 站点品牌名(默认取环境变量,五站登录页各不相同) */
|
/** 站点品牌名 */
|
||||||
brandName?: string;
|
brandName?: string;
|
||||||
/** 品牌侧栏标语 */
|
/** 品牌侧栏标语 */
|
||||||
brandTagline?: string;
|
brandTagline?: string;
|
||||||
/** 品牌强调色 HSL 三元组(如 "16 85% 55%"),传入即启用分栏品牌模板 */
|
/** 品牌强调色 HSL 三元组 */
|
||||||
accent?: string;
|
accent?: string;
|
||||||
|
/** 登录布局变体:五种站点五种形态 */
|
||||||
|
variant?: "glass" | "gradient" | "editorial" | "terminal" | "minimal" | "split";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # 组件 FormShell
|
||||||
|
* 说明:认证表单外壳 —— 六种布局形态,五个网站各用一种:
|
||||||
|
* - glass 深蓝渐变全屏 + 玻璃拟态浮动卡片(AI Stack)
|
||||||
|
* - gradient 品牌色渐变全屏 + 白色大圆角卡片(LaunchAI)
|
||||||
|
* - editorial 报纸头版:衬线居中报头 + 下划线输入 + 方角黑按钮(The AI Ledger)
|
||||||
|
* - terminal 深色终端窗口 + 等宽输入 + 命令行按钮(DevAI.tools)
|
||||||
|
* - minimal 纯白极简居中 + 大圆角搜索式输入(findai)
|
||||||
|
* - split 经典左右分栏(后备)
|
||||||
|
*/
|
||||||
export function FormShell<TFieldValues extends FieldValues>({
|
export function FormShell<TFieldValues extends FieldValues>({
|
||||||
form,
|
form,
|
||||||
title,
|
title,
|
||||||
@@ -48,76 +59,206 @@ export function FormShell<TFieldValues extends FieldValues>({
|
|||||||
brandName,
|
brandName,
|
||||||
brandTagline,
|
brandTagline,
|
||||||
accent,
|
accent,
|
||||||
|
variant,
|
||||||
}: FormShellProps<TFieldValues>) {
|
}: FormShellProps<TFieldValues>) {
|
||||||
const t = useTranslations('auth.social');
|
const t = useTranslations('auth.social');
|
||||||
const brand = brandName || process.env.NEXT_PUBLIC_APP_NAME || 'AI Directory';
|
const brand = brandName || process.env.NEXT_PUBLIC_APP_NAME || 'AI Directory';
|
||||||
// 品牌模式用站点品牌字标(不传 headerSlot 时),非品牌模式保留原 Logo
|
const tagline = brandTagline || `Welcome back to ${brand}.`;
|
||||||
const headerContent =
|
const accentHsl = accent ? `hsl(${accent})` : 'hsl(222 47% 11%)';
|
||||||
headerSlot !== undefined
|
const initial = brand.slice(0, 1);
|
||||||
? headerSlot
|
|
||||||
: accent
|
|
||||||
? <span className="inline-flex items-center gap-2 text-lg font-bold text-foreground">
|
|
||||||
<span className="flex h-8 w-8 items-center justify-center rounded-lg text-white" style={{ backgroundColor: `hsl(${accent})` }}>
|
|
||||||
{brand.slice(0, 1)}
|
|
||||||
</span>
|
|
||||||
{brand}
|
|
||||||
</span>
|
|
||||||
: <Logo />;
|
|
||||||
|
|
||||||
// 品牌模板:传入 accent 时渲染分栏布局(左侧品牌区 / 右侧表单),每站视觉不同
|
const errorBox = error ? (
|
||||||
const formColumn = (
|
<div className="rounded-md bg-destructive/10 p-3 text-sm text-red-600" role="alert">
|
||||||
<div className="mx-auto w-full max-w-md">
|
{error}
|
||||||
<div>
|
|
||||||
<div className="flex">{headerContent}</div>
|
|
||||||
<h2 className="mt-8 text-3xl font-bold leading-9 tracking-tight text-foreground">
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
{description ? (
|
|
||||||
<p className="mt-2 text-[0.95rem] leading-6 text-muted-foreground">{description}</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8">
|
) : null;
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
{socialSlot}
|
const submitBtn = (cls: string) => (
|
||||||
{socialSlot ? (
|
<button type="submit" disabled={isLoading} className={cls}>
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className="h-px flex-1 bg-border" />
|
|
||||||
<span className="text-xs text-muted-foreground">{t('orContinueWith')}</span>
|
|
||||||
<span className="h-px flex-1 bg-border" />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{children}
|
|
||||||
{error ? (
|
|
||||||
<div className="rounded-md bg-destructive/10 p-3 text-[0.95rem] text-red-600">{error}</div>
|
|
||||||
) : null}
|
|
||||||
<Button className="w-full" disabled={isLoading} type="submit">
|
|
||||||
{isLoading ? submitLoadingText ?? submitText : submitText}
|
{isLoading ? submitLoadingText ?? submitText : submitText}
|
||||||
</Button>
|
</button>
|
||||||
</form>
|
|
||||||
{footer}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (accent) {
|
const footerNode = footer ? <div className="mt-6">{footer}</div> : null;
|
||||||
|
|
||||||
|
/* ================= 1. AI Stack:深蓝渐变 + 玻璃卡片 ================= */
|
||||||
|
if (variant === "glass") {
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
{/* 品牌分栏模板:col-span 全宽(覆盖 AuthLayout 的 2 列网格),左品牌右表单 */}
|
<div className="relative flex min-h-screen w-full items-center justify-center overflow-hidden bg-[linear-gradient(160deg,hsl(224_60%_12%),hsl(224_55%_22%)_60%,hsl(210_45%_30%))] px-4 py-12">
|
||||||
<div className={cn("col-span-full grid min-h-screen w-full lg:grid-cols-2", className)}>
|
<div className="pointer-events-none absolute -left-40 -top-40 h-96 w-96 rounded-full bg-blue-400/10 blur-3xl" />
|
||||||
|
<div className="pointer-events-none absolute -bottom-40 -right-40 h-96 w-96 rounded-full bg-indigo-400/10 blur-3xl" />
|
||||||
|
<div className="relative w-full max-w-md rounded-2xl border border-white/15 bg-white/[0.08] p-8 shadow-2xl backdrop-blur-xl sm:p-10">
|
||||||
|
<div className="mb-8 flex items-center gap-2.5">
|
||||||
|
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-white/15 text-lg font-bold text-white">
|
||||||
|
{initial}
|
||||||
|
</span>
|
||||||
|
<span className="text-lg font-bold text-white">{brand}</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-bold text-white">{title}</h1>
|
||||||
|
{description && <p className="mt-2 text-sm text-white/60">{description}</p>}
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-8 space-y-5">
|
||||||
|
{socialSlot}
|
||||||
|
{children}
|
||||||
|
{errorBox}
|
||||||
|
{submitBtn("w-full rounded-xl bg-white py-3 text-sm font-semibold text-slate-900 transition hover:bg-white/90 disabled:opacity-60")}
|
||||||
|
</form>
|
||||||
|
{footerNode}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 2. LaunchAI:品牌渐变 + 白色大圆角卡片 ================= */
|
||||||
|
if (variant === "gradient") {
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<div
|
||||||
|
className="relative flex min-h-screen w-full items-center justify-center px-4 py-12"
|
||||||
|
style={{ background: `linear-gradient(140deg, hsl(${accent}) 0%, hsl(28 90% 55%) 100%)` }}
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-md rounded-3xl bg-white p-8 shadow-[0_24px_70px_rgba(0,0,0,0.25)] sm:p-10">
|
||||||
|
<div className="mb-7 flex items-center gap-2.5">
|
||||||
|
<span
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-2xl text-lg font-bold text-white"
|
||||||
|
style={{ backgroundColor: accentHsl }}
|
||||||
|
>
|
||||||
|
{initial}
|
||||||
|
</span>
|
||||||
|
<span className="text-xl font-extrabold tracking-tight text-foreground">{brand}</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl font-extrabold tracking-tight text-foreground">{title}</h1>
|
||||||
|
{description && <p className="mt-2 text-sm text-muted-foreground">{description}</p>}
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-7 space-y-5">
|
||||||
|
{socialSlot}
|
||||||
|
{children}
|
||||||
|
{errorBox}
|
||||||
|
{submitBtn(`w-full rounded-2xl py-3.5 text-base font-bold text-white transition hover:opacity-90 disabled:opacity-60`)}
|
||||||
|
</form>
|
||||||
|
{footerNode}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 3. The AI Ledger:报纸头版 + 下划线输入 ================= */
|
||||||
|
if (variant === "editorial") {
|
||||||
|
const lineInput =
|
||||||
|
"w-full border-0 border-b-2 border-neutral-300 bg-transparent px-0 py-2 text-lg font-serif text-neutral-900 placeholder:text-neutral-400 focus:border-neutral-900 focus:outline-none rounded-none";
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<div className="min-h-screen w-full bg-[hsl(42_45%_97%)] px-4 py-10 font-serif text-neutral-900">
|
||||||
|
<div className="mx-auto max-w-xl">
|
||||||
|
{/* 报头 */}
|
||||||
|
<div className="border-y-[3px] border-double border-neutral-900 py-3 text-center">
|
||||||
|
<p className="text-[10px] uppercase tracking-[0.35em] text-neutral-500">
|
||||||
|
{new Date().toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", year: "numeric" })}
|
||||||
|
</p>
|
||||||
|
<h1 className="mt-1 text-4xl font-black tracking-tight">{brand}</h1>
|
||||||
|
<p className="mt-1 text-[10px] uppercase tracking-[0.35em] text-neutral-500">{tagline}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="mt-10 text-center text-2xl font-bold">{title}</h2>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-8 space-y-6">
|
||||||
|
{socialSlot}
|
||||||
|
{children}
|
||||||
|
{errorBox}
|
||||||
|
{submitBtn("w-full border-2 border-neutral-900 bg-neutral-900 py-3 text-sm font-bold uppercase tracking-[0.2em] text-white transition hover:bg-transparent hover:text-neutral-900 disabled:opacity-60")}
|
||||||
|
</form>
|
||||||
|
{footerNode}
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto mt-12 max-w-xl border-t border-neutral-300 pt-4 text-center text-[10px] uppercase tracking-[0.3em] text-neutral-400">
|
||||||
|
© {new Date().getFullYear()} {brand}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 4. DevAI.tools:深色终端窗口 ================= */
|
||||||
|
if (variant === "terminal") {
|
||||||
|
const termInput =
|
||||||
|
"w-full rounded border border-emerald-500/30 bg-black/50 px-3 py-2.5 font-mono text-sm text-emerald-100 placeholder:text-emerald-100/30 focus:border-emerald-400/70 focus:outline-none rounded-md";
|
||||||
|
const termLabel = "mb-1 block font-mono text-xs text-emerald-500/80";
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<div className="flex min-h-screen w-full items-center justify-center bg-[hsl(220_25%_7%)] px-4 py-12">
|
||||||
|
<div className="w-full max-w-lg overflow-hidden rounded-lg border border-emerald-500/20 bg-[hsl(220_25%_9%)] shadow-[0_0_40px_rgba(16,185,129,0.06)]">
|
||||||
|
{/* 终端标题栏 */}
|
||||||
|
<div className="flex items-center gap-2 border-b border-emerald-500/15 bg-black/40 px-4 py-3">
|
||||||
|
<span className="h-3 w-3 rounded-full bg-red-500/80" />
|
||||||
|
<span className="h-3 w-3 rounded-full bg-yellow-500/80" />
|
||||||
|
<span className="h-3 w-3 rounded-full bg-emerald-500/80" />
|
||||||
|
<span className="ml-3 font-mono text-xs text-emerald-100/50">{brand.toLowerCase()} — auth</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-7">
|
||||||
|
<p className="font-mono text-sm text-emerald-400/90">
|
||||||
|
<span className="text-emerald-600">$</span> {brand.toLowerCase()} auth --login
|
||||||
|
</p>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-6 space-y-5">
|
||||||
|
{socialSlot}
|
||||||
|
{children}
|
||||||
|
{errorBox}
|
||||||
|
{submitBtn("w-full rounded-md bg-emerald-500 py-3 font-mono text-sm font-bold text-black transition hover:bg-emerald-400 disabled:opacity-60")}
|
||||||
|
</form>
|
||||||
|
{footerNode}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 5. findai:纯白极简居中 ================= */
|
||||||
|
if (variant === "minimal") {
|
||||||
|
const softInput =
|
||||||
|
"w-full rounded-2xl border border-transparent bg-neutral-100 px-5 py-4 text-base text-foreground placeholder:text-neutral-400 focus:border-violet-300 focus:bg-white focus:outline-none focus:ring-4 focus:ring-violet-500/10 transition rounded-2xl";
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<div className="flex min-h-screen w-full flex-col items-center justify-center bg-white px-4 py-12">
|
||||||
|
<span className="flex h-12 w-12 items-center justify-center rounded-2xl bg-violet-600 text-xl font-bold text-white">
|
||||||
|
{initial}
|
||||||
|
</span>
|
||||||
|
<h1 className="mt-6 text-3xl font-bold tracking-tight text-foreground">{brand}</h1>
|
||||||
|
<p className="mt-2 text-sm text-neutral-400">{tagline}</p>
|
||||||
|
<div className="mt-10 w-full max-w-sm">
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
{socialSlot}
|
||||||
|
{children}
|
||||||
|
{errorBox}
|
||||||
|
{submitBtn("w-full rounded-2xl bg-violet-600 py-4 text-base font-semibold text-white transition hover:bg-violet-700 disabled:opacity-60")}
|
||||||
|
</form>
|
||||||
|
{footerNode}
|
||||||
|
</div>
|
||||||
|
<p className="mt-16 text-xs text-neutral-300">{brand} — search, ranked by people</p>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 后备:左右分栏 ================= */
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"grid min-h-screen w-full lg:grid-cols-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="relative hidden flex-col justify-between overflow-hidden p-12 lg:flex"
|
className="relative hidden flex-col justify-between overflow-hidden p-12 lg:flex"
|
||||||
style={{ backgroundColor: `hsl(${accent})` }}
|
style={{ backgroundColor: accentHsl }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-xl font-bold text-white">
|
<div className="flex items-center gap-2 text-xl font-bold text-white">
|
||||||
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-white/20 text-lg">
|
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-white/20 text-lg">
|
||||||
{brand.slice(0, 1)}
|
{initial}
|
||||||
</span>
|
</span>
|
||||||
{brand}
|
{brand}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="max-w-sm text-2xl font-semibold leading-snug text-white">
|
<p className="max-w-sm text-2xl font-semibold leading-snug text-white">{tagline}</p>
|
||||||
{brandTagline || `Welcome back to ${brand}.`}
|
|
||||||
</p>
|
|
||||||
<p className="mt-3 max-w-sm text-sm leading-relaxed text-white/70">
|
<p className="mt-3 max-w-sm text-sm leading-relaxed text-white/70">
|
||||||
Vote, review and save the AI tools you love — join the community.
|
Vote, review and save the AI tools you love — join the community.
|
||||||
</p>
|
</p>
|
||||||
@@ -125,23 +266,25 @@ export function FormShell<TFieldValues extends FieldValues>({
|
|||||||
<p className="text-xs text-white/50">© {new Date().getFullYear()} {brand}</p>
|
<p className="text-xs text-white/50">© {new Date().getFullYear()} {brand}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center px-4 py-12 sm:px-6 lg:px-20 xl:px-24">
|
<div className="flex items-center justify-center px-4 py-12 sm:px-6 lg:px-20 xl:px-24">
|
||||||
{formColumn}
|
<div className="mx-auto w-full max-w-md">
|
||||||
|
<span className="inline-flex items-center gap-2 text-lg font-bold text-foreground">
|
||||||
|
<span className="flex h-8 w-8 items-center justify-center rounded-lg text-white" style={{ backgroundColor: accentHsl }}>
|
||||||
|
{initial}
|
||||||
|
</span>
|
||||||
|
{brand}
|
||||||
|
</span>
|
||||||
|
<h2 className="mt-8 text-3xl font-bold tracking-tight text-foreground">{title}</h2>
|
||||||
|
{description && <p className="mt-2 text-[0.95rem] text-muted-foreground">{description}</p>}
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-8 space-y-5">
|
||||||
|
{socialSlot}
|
||||||
|
{children}
|
||||||
|
{errorBox}
|
||||||
|
{submitBtn("w-full rounded-xl bg-primary py-3 text-sm font-semibold text-primary-foreground transition hover:opacity-90 disabled:opacity-60")}
|
||||||
|
</form>
|
||||||
|
{footerNode}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
|
||||||
<Form {...form}>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex w-full items-center justify-center px-4 py-12 sm:px-6 lg:flex-none lg:px-20 xl:px-24",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{formColumn}
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type FormTextFieldProps<TFieldValues extends FieldValues> = BaseProps &
|
|||||||
};
|
};
|
||||||
|
|
||||||
const inputClasses =
|
const inputClasses =
|
||||||
"block w-full rounded-md border-0 bg-input px-4 py-1.5 text-foreground shadow-aceternity placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring";
|
"auth-input block w-full rounded-md border-0 bg-input px-4 py-2.5 text-[0.95rem] text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-colors";
|
||||||
|
|
||||||
export function FormTextField<TFieldValues extends FieldValues>({
|
export function FormTextField<TFieldValues extends FieldValues>({
|
||||||
control,
|
control,
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export interface SiteConfig {
|
|||||||
accent?: string;
|
accent?: string;
|
||||||
/** 站点视觉签名 class(挂载在站点外壳根元素,驱动全站差异化样式) */
|
/** 站点视觉签名 class(挂载在站点外壳根元素,驱动全站差异化样式) */
|
||||||
themeClass?: string;
|
themeClass?: string;
|
||||||
|
/** 登录页布局变体:五站五种形态 */
|
||||||
|
authVariant?: "glass" | "gradient" | "editorial" | "terminal" | "minimal" | "split";
|
||||||
/** 中文品牌文案(导航/页脚/hero/搜索占位) */
|
/** 中文品牌文案(导航/页脚/hero/搜索占位) */
|
||||||
brandZh?: {
|
brandZh?: {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* # 组件 AuthLayout
|
* # 组件 AuthLayout
|
||||||
* 说明:认证页容器。布局职责已下沉到 FormShell(品牌分栏/居中两套模板),
|
* 说明:认证页容器。布局职责在 FormShell(六种布局变体);
|
||||||
* 这里只提供页面底色,避免双层 grid 叠加导致的多列错位。
|
* 这里注入 data-theme(由 NEXT_PUBLIC_AUTH_VARIANT 映射),
|
||||||
|
* 使输入框/标签/按钮的站点主题化样式(globals.css)在 auth 路由组生效。
|
||||||
*/
|
*/
|
||||||
|
const VARIANT_THEME: Record<string, string> = {
|
||||||
|
glass: "ai-stack",
|
||||||
|
gradient: "launchai",
|
||||||
|
editorial: "the-ai-ledger",
|
||||||
|
terminal: "devai-tools",
|
||||||
|
minimal: "findai",
|
||||||
|
split: "ai-stack",
|
||||||
|
};
|
||||||
|
|
||||||
export function AuthLayout({ children }: { children: React.ReactNode }) {
|
export function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const variant = process.env.NEXT_PUBLIC_AUTH_VARIANT || "glass";
|
||||||
return (
|
return (
|
||||||
<div className={cn("w-full min-h-screen bg-background")}>
|
<div
|
||||||
|
data-theme={VARIANT_THEME[variant] ?? "ai-stack"}
|
||||||
|
className="min-h-screen w-full"
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -383,3 +383,80 @@ html {
|
|||||||
background: hsl(var(--background));
|
background: hsl(var(--background));
|
||||||
clip-path: polygon(0 100%, 100% 0, 100% 100%, 0 100%);
|
clip-path: polygon(0 100%, 100% 0, 100% 100%, 0 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
认证页主题化:输入框 / 标签 / 社交按钮按站点形态变化
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
/* The AI Ledger:下划线输入 + 衬线 + 方角按钮 */
|
||||||
|
[data-theme="the-ai-ledger"] .auth-input {
|
||||||
|
border-radius: 0;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 2px solid hsl(24 10% 70%);
|
||||||
|
background: transparent;
|
||||||
|
font-family: Georgia, "Times New Roman", serif;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
[data-theme="the-ai-ledger"] .auth-input:focus {
|
||||||
|
border-bottom-color: hsl(24 10% 12%);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
[data-theme="the-ai-ledger"] form button[type="submit"],
|
||||||
|
[data-theme="the-ai-ledger"] form .rounded-lg {
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
[data-theme="the-ai-ledger"] form label {
|
||||||
|
font-family: Georgia, serif;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DevAI.tools:终端等宽输入 */
|
||||||
|
[data-theme="devai-tools"] .auth-input {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: 1px solid hsl(150 60% 45% / 0.3);
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||||
|
color: hsl(150 40% 88%);
|
||||||
|
}
|
||||||
|
[data-theme="devai-tools"] .auth-input::placeholder {
|
||||||
|
color: hsl(150 30% 50% / 0.4);
|
||||||
|
}
|
||||||
|
[data-theme="devai-tools"] .auth-input:focus {
|
||||||
|
border-color: hsl(150 60% 45% / 0.7);
|
||||||
|
box-shadow: 0 0 0 3px hsl(150 60% 45% / 0.1);
|
||||||
|
}
|
||||||
|
[data-theme="devai-tools"] form label {
|
||||||
|
font-family: ui-monospace, Menlo, monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: hsl(150 40% 60% / 0.85);
|
||||||
|
}
|
||||||
|
[data-theme="devai-tools"] form .rounded-lg {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border-color: hsl(150 60% 45% / 0.25);
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* findai:大圆角浅灰底输入 */
|
||||||
|
[data-theme="findai"] .auth-input {
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: hsl(0 0% 96%);
|
||||||
|
}
|
||||||
|
[data-theme="findai"] .auth-input:focus {
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 0 0 4px hsl(262 65% 52% / 0.1);
|
||||||
|
}
|
||||||
|
[data-theme="findai"] form .rounded-lg {
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LaunchAI:大圆角输入 */
|
||||||
|
[data-theme="launchai"] .auth-input {
|
||||||
|
border-radius: 0.875rem;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
[data-theme="launchai"] form .rounded-lg {
|
||||||
|
border-radius: 0.875rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ NEXT_PUBLIC_APP_URL="http://localhost:3001"
|
|||||||
NEXT_PUBLIC_APP_NAME="AI Stack"
|
NEXT_PUBLIC_APP_NAME="AI Stack"
|
||||||
# 登录页品牌模板(分栏侧栏颜色与标语)
|
# 登录页品牌模板(分栏侧栏颜色与标语)
|
||||||
NEXT_PUBLIC_AUTH_ACCENT="222 47% 11%"
|
NEXT_PUBLIC_AUTH_ACCENT="222 47% 11%"
|
||||||
|
NEXT_PUBLIC_AUTH_VARIANT="glass"
|
||||||
NEXT_PUBLIC_AUTH_BRAND_TAGLINE="The professional AI tools directory — ranked by the community."
|
NEXT_PUBLIC_AUTH_BRAND_TAGLINE="The professional AI tools directory — ranked by the community."
|
||||||
# 开发阶段免邮箱验证(生产置 false 发送真实验证邮件)
|
# 开发阶段免邮箱验证(生产置 false 发送真实验证邮件)
|
||||||
DEV_SKIP_EMAIL_VERIFY="true"
|
DEV_SKIP_EMAIL_VERIFY="true"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { SiteConfig } from "@/kit/types";
|
|||||||
|
|
||||||
// AI Stack —— 独立站点的全部品牌/导航/页脚配置
|
// AI Stack —— 独立站点的全部品牌/导航/页脚配置
|
||||||
export const siteConfig: SiteConfig = {
|
export const siteConfig: SiteConfig = {
|
||||||
|
"authVariant": "glass",
|
||||||
"themeClass": "site-ai-stack",
|
"themeClass": "site-ai-stack",
|
||||||
"brandZh": {
|
"brandZh": {
|
||||||
"name": "AI Stack",
|
"name": "AI Stack",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ NEXT_PUBLIC_APP_URL="http://localhost:3004"
|
|||||||
NEXT_PUBLIC_APP_NAME="DevAI.tools"
|
NEXT_PUBLIC_APP_NAME="DevAI.tools"
|
||||||
# 登录页品牌模板(分栏侧栏颜色与标语)
|
# 登录页品牌模板(分栏侧栏颜色与标语)
|
||||||
NEXT_PUBLIC_AUTH_ACCENT="150 60% 30%"
|
NEXT_PUBLIC_AUTH_ACCENT="150 60% 30%"
|
||||||
|
NEXT_PUBLIC_AUTH_VARIANT="terminal"
|
||||||
NEXT_PUBLIC_AUTH_BRAND_TAGLINE="AI tools for developers — curated, tested, ranked by the community."
|
NEXT_PUBLIC_AUTH_BRAND_TAGLINE="AI tools for developers — curated, tested, ranked by the community."
|
||||||
# 开发阶段免邮箱验证(生产置 false 发送真实验证邮件)
|
# 开发阶段免邮箱验证(生产置 false 发送真实验证邮件)
|
||||||
DEV_SKIP_EMAIL_VERIFY="true"
|
DEV_SKIP_EMAIL_VERIFY="true"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { SiteConfig } from "@/kit/types";
|
|||||||
|
|
||||||
// DevAI.tools —— 独立站点的全部品牌/导航/页脚配置
|
// DevAI.tools —— 独立站点的全部品牌/导航/页脚配置
|
||||||
export const siteConfig: SiteConfig = {
|
export const siteConfig: SiteConfig = {
|
||||||
|
"authVariant": "terminal",
|
||||||
"themeClass": "site-devai-tools",
|
"themeClass": "site-devai-tools",
|
||||||
"brandZh": {
|
"brandZh": {
|
||||||
"name": "DevAI.tools",
|
"name": "DevAI.tools",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ NEXT_PUBLIC_APP_URL="http://localhost:3005"
|
|||||||
NEXT_PUBLIC_APP_NAME="findai"
|
NEXT_PUBLIC_APP_NAME="findai"
|
||||||
# 登录页品牌模板(分栏侧栏颜色与标语)
|
# 登录页品牌模板(分栏侧栏颜色与标语)
|
||||||
NEXT_PUBLIC_AUTH_ACCENT="260 70% 52%"
|
NEXT_PUBLIC_AUTH_ACCENT="260 70% 52%"
|
||||||
|
NEXT_PUBLIC_AUTH_VARIANT="minimal"
|
||||||
NEXT_PUBLIC_AUTH_BRAND_TAGLINE="The minimal AI tool search engine."
|
NEXT_PUBLIC_AUTH_BRAND_TAGLINE="The minimal AI tool search engine."
|
||||||
# 开发阶段免邮箱验证(生产置 false 发送真实验证邮件)
|
# 开发阶段免邮箱验证(生产置 false 发送真实验证邮件)
|
||||||
DEV_SKIP_EMAIL_VERIFY="true"
|
DEV_SKIP_EMAIL_VERIFY="true"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { SiteConfig } from "@/kit/types";
|
|||||||
|
|
||||||
// findai —— 独立站点的全部品牌/导航/页脚配置
|
// findai —— 独立站点的全部品牌/导航/页脚配置
|
||||||
export const siteConfig: SiteConfig = {
|
export const siteConfig: SiteConfig = {
|
||||||
|
"authVariant": "minimal",
|
||||||
"themeClass": "site-findai",
|
"themeClass": "site-findai",
|
||||||
"brandZh": {
|
"brandZh": {
|
||||||
"name": "findai",
|
"name": "findai",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ NEXT_PUBLIC_APP_URL="http://localhost:3002"
|
|||||||
NEXT_PUBLIC_APP_NAME="LaunchAI"
|
NEXT_PUBLIC_APP_NAME="LaunchAI"
|
||||||
# 登录页品牌模板(分栏侧栏颜色与标语)
|
# 登录页品牌模板(分栏侧栏颜色与标语)
|
||||||
NEXT_PUBLIC_AUTH_ACCENT="16 85% 55%"
|
NEXT_PUBLIC_AUTH_ACCENT="16 85% 55%"
|
||||||
|
NEXT_PUBLIC_AUTH_VARIANT="gradient"
|
||||||
NEXT_PUBLIC_AUTH_BRAND_TAGLINE="Discover and upvote today's AI products."
|
NEXT_PUBLIC_AUTH_BRAND_TAGLINE="Discover and upvote today's AI products."
|
||||||
# 开发阶段免邮箱验证(生产置 false 发送真实验证邮件)
|
# 开发阶段免邮箱验证(生产置 false 发送真实验证邮件)
|
||||||
DEV_SKIP_EMAIL_VERIFY="true"
|
DEV_SKIP_EMAIL_VERIFY="true"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { SiteConfig } from "@/kit/types";
|
|||||||
|
|
||||||
// LaunchAI —— 独立站点的全部品牌/导航/页脚配置
|
// LaunchAI —— 独立站点的全部品牌/导航/页脚配置
|
||||||
export const siteConfig: SiteConfig = {
|
export const siteConfig: SiteConfig = {
|
||||||
|
"authVariant": "gradient",
|
||||||
"themeClass": "site-launchai",
|
"themeClass": "site-launchai",
|
||||||
"brandZh": {
|
"brandZh": {
|
||||||
"name": "LaunchAI",
|
"name": "LaunchAI",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ NEXT_PUBLIC_APP_URL="http://localhost:3003"
|
|||||||
NEXT_PUBLIC_APP_NAME="The AI Ledger"
|
NEXT_PUBLIC_APP_NAME="The AI Ledger"
|
||||||
# 登录页品牌模板(分栏侧栏颜色与标语)
|
# 登录页品牌模板(分栏侧栏颜色与标语)
|
||||||
NEXT_PUBLIC_AUTH_ACCENT="36 70% 40%"
|
NEXT_PUBLIC_AUTH_ACCENT="36 70% 40%"
|
||||||
|
NEXT_PUBLIC_AUTH_VARIANT="editorial"
|
||||||
NEXT_PUBLIC_AUTH_BRAND_TAGLINE="Independent journalism on AI tools, with a community-ranked directory."
|
NEXT_PUBLIC_AUTH_BRAND_TAGLINE="Independent journalism on AI tools, with a community-ranked directory."
|
||||||
# 开发阶段免邮箱验证(生产置 false 发送真实验证邮件)
|
# 开发阶段免邮箱验证(生产置 false 发送真实验证邮件)
|
||||||
DEV_SKIP_EMAIL_VERIFY="true"
|
DEV_SKIP_EMAIL_VERIFY="true"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { SiteConfig } from "@/kit/types";
|
|||||||
|
|
||||||
// The AI Ledger —— 独立站点的全部品牌/导航/页脚配置
|
// The AI Ledger —— 独立站点的全部品牌/导航/页脚配置
|
||||||
export const siteConfig: SiteConfig = {
|
export const siteConfig: SiteConfig = {
|
||||||
|
"authVariant": "editorial",
|
||||||
"themeClass": "site-the-ai-ledger",
|
"themeClass": "site-the-ai-ledger",
|
||||||
"brandZh": {
|
"brandZh": {
|
||||||
"name": "The AI Ledger",
|
"name": "The AI Ledger",
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ async function SiteHome({ locale }: { locale: string }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Masthead */}
|
{/* Masthead */}
|
||||||
<section className="border-b border-border">
|
<section className="hero-mast border-b border-border">
|
||||||
<div className="mx-auto max-w-4xl px-4 py-16 text-center sm:px-6 md:py-20">
|
<div className="mx-auto max-w-4xl px-4 py-16 text-center sm:px-6 md:py-20">
|
||||||
<p className="text-xs font-medium uppercase tracking-[0.3em] text-muted-foreground">
|
<p className="text-xs font-medium uppercase tracking-[0.3em] text-muted-foreground">
|
||||||
{t("mastheadLabel")}
|
{t("mastheadLabel")}
|
||||||
|
|||||||
Reference in New Issue
Block a user