feat(assets): add initial fonts and image assets

This commit is contained in:
2026-06-09 15:47:20 +08:00
parent cda55c8f9b
commit e2f60bc4f2
41 changed files with 207 additions and 70 deletions
+35 -1
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import localFont from "next/font/local";
import Script from "next/script";
import "./globals.css";
@@ -15,6 +16,39 @@ const geistMono = Geist_Mono({
subsets: ["latin"],
});
/**
* Athelas 字体加载
* 原始资源:/Users/chase/Documents/cozsweet/fonts/Athelas-*.ttf
* 放置位置:/public/fonts/Athelas-*.ttf
*
* 路径计算(`next/font/local` 要求相对路径从调用文件出发):
* src/app/layout.tsx → ../../public/fonts/...
*
* 通过 `next/font/local` 自动 preload、压缩、inline。
* 输出 CSS 变量 `--font-athelas`,由 `globals.css` 桥接到 Tailwind `font-athelas`。
*/
const athelas = localFont({
src: [
{
path: "../../public/fonts/Athelas-Regular.ttf",
weight: "400",
style: "normal",
},
{
path: "../../public/fonts/Athelas-Bold.ttf",
weight: "700",
style: "normal",
},
{
path: "../../public/fonts/Athelas-BoldItalic.ttf",
weight: "700",
style: "italic",
},
],
variable: "--font-athelas",
display: "swap",
});
export const metadata: Metadata = {
title: "cozsweet",
description: "cozsweet frontend (Next.js)",
@@ -31,7 +65,7 @@ export default function RootLayout({
<html
lang="en"
suppressHydrationWarning
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
className={`${geistSans.variable} ${geistMono.variable} ${athelas.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">
{/* Facebook SDK JSauth + splash 入口会用到 FB.login */}
@@ -1,8 +1,12 @@
.bg {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 80% 20%, rgba(248, 77, 150, 0.25), transparent 60%),
linear-gradient(180deg, #1a0f2a 0%, #0d0b14 100%);
z-index: 0;
background: var(--color-sidebar-background);
overflow: hidden;
}
.image {
object-fit: cover;
object-position: center;
}
@@ -1,14 +1,18 @@
"use client";
/**
* Splash 背景
*
* 原始 Dart: lib/ui/splash/widgets/splash_background.dart
* 使用 `Assets.images.picBgHome.image(fit: BoxFit.cover)` 渲染全屏背景图。
*
* 当前实现:纯色 + CSS 渐变占位(待 `public/splash/bg.png` 资源到位后可切换为 `<Image>`)。
*/
import Image from "next/image";
import styles from "./splash-background.module.css";
export function SplashBackground() {
return <div className={styles.bg} aria-hidden="true" />;
return (
<div className={styles.bg}>
<Image
src="/images/splash/pic-bg-home.png"
alt=""
fill
priority
sizes="100vw"
className={styles.image}
/>
</div>
);
}
@@ -1,7 +1,61 @@
.wrapper {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
flex-direction: row;
align-items: center;
justify-content: center;
gap: var(--spacing-26);
width: 100%;
z-index: 2;
}
.skip {
font-family: var(--font-athelas);
font-size: var(--font-size-xxl);
font-weight: 700;
color: #ffffff;
background: transparent;
border: 0;
cursor: pointer;
padding: 0;
}
.skip:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.separator {
width: var(--spacing-26);
height: 1px;
}
.facebookButton {
height: 48px;
padding: 0 30px;
border: 0;
border-radius: 24px;
background: linear-gradient(
to right,
#fff96ade,
#fff657a0
);
color: #ffffff;
font-weight: 700;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 10px rgba(248, 89, 168, 0.3);
}
.facebookButton:disabled {
cursor: not-allowed;
opacity: 0.7;
}
.facebookLabel {
font-family: var(--font-athelas);
font-size: var(--font-size-xxl);
font-weight: 700;
color: #ffffff;
}
+45 -17
View File
@@ -1,35 +1,63 @@
"use client";
/**
* Splash 底部按钮组Skip + Facebook 登录)
* Splash 底部按钮组
*
* 原始 Dart: lib/ui/splash/widgets/splash_button.dart
* `Row(children: [Skip 文本, SizedBox 26, Facebook 登录按钮])`
*
* 关键对齐点:
* - 仅两个元素:Skip 文本 + Facebook 登录按钮
* - 居中布局 (MainAxisAlignment.center)
* - Facebook 按钮:带渐变(primaryGradient+ 圆角 24 + 高度 48
* - 加载中:按钮内显示 spinner
*/
import { useAuthState } from "@/contexts/auth/auth-context";
import { AuthSocialButtons } from "@/app/auth/components/auth-social-buttons";
import { AuthPrimaryButton } from "@/app/auth/components/auth-primary-button";
import { AuthDivider } from "@/app/auth/components/auth-divider";
import { useAuthDispatch } from "@/contexts/auth/auth-context";
import { useAuthState, useAuthDispatch } from "@/contexts/auth/auth-context";
import { LoadingIndicator } from "@/app/_components/core/loading-indicator";
import styles from "./splash-button.module.css";
export interface SplashButtonProps {
onSkip: () => void;
onFacebookLogin: () => void;
}
export function SplashButton({ onSkip }: SplashButtonProps) {
export function SplashButton({
onSkip,
onFacebookLogin,
}: SplashButtonProps) {
const state = useAuthState();
const dispatch = useAuthDispatch();
const isLoading = state.isLoading;
return (
<div className={styles.wrapper}>
<AuthSocialButtons
loadingProvider={state.isLoading ? "facebook" : null}
onFacebook={() => dispatch({ type: "AuthFacebookLoginSubmitted" })}
onGoogle={() => dispatch({ type: "AuthGoogleLoginSubmitted" })}
/>
<AuthDivider label="or" />
<AuthPrimaryButton onClick={onSkip} variant="primary">
Skip Continue as Guest
</AuthPrimaryButton>
{/* Skip 文本按钮 */}
<button
type="button"
onClick={onSkip}
className={styles.skip}
disabled={isLoading}
>
Skip
</button>
<div className={styles.separator} aria-hidden="true" />
{/* Facebook 登录按钮(带渐变) */}
<button
type="button"
onClick={() => {
dispatch({ type: "AuthFacebookLoginSubmitted" });
onFacebookLogin();
}}
disabled={isLoading}
className={styles.facebookButton}
>
{isLoading ? (
<LoadingIndicator color="#ffffff" />
) : (
<span className={styles.facebookLabel}>Login with Facebook</span>
)}
</button>
</div>
);
}
@@ -2,21 +2,17 @@
display: flex;
flex-direction: column;
gap: var(--spacing-md);
color: var(--color-text-primary);
z-index: 2;
}
.title {
margin: 0;
font-family: var(--font-athelas);
font-size: var(--font-size-26);
font-weight: 600;
line-height: 1.3;
}
.subtitle {
font-size: var(--font-size-title);
font-weight: 700;
font-style: italic;
line-height: 1.4;
color: #ffffff;
text-align: left;
white-space: pre-line;
margin: 0;
font-size: var(--font-size-md);
line-height: 1.5;
color: rgba(255, 255, 255, 0.8);
}
+14 -5
View File
@@ -1,18 +1,27 @@
"use client";
/**
* Splash 内容文案
* Splash 内容文案
*
* 原始 Dart: lib/ui/splash/widgets/splash_content.dart
*
* 关键差异:
* - 使用 Athelas 字体(衬线体)
* - 标题加粗 + 斜体(与原 Dart 一致)
* - 颜色:白色(背景图上需高对比度)
*/
import styles from "./splash-content.module.css";
const APOSTROPHE = "&#39;";
export function SplashContent() {
return (
<div className={styles.content}>
<h1 className={styles.title}>Chat with Elio, anytime, anywhere</h1>
<p className={styles.subtitle}>
A safe, private space to talk, share, and unwind your AI companion who&apos;s always there.
</p>
<h1 className={styles.title}>
Welcome to my secret hideout~
{"\n"}It{APOSTROPHE}s just the two of us now.
{"\n"}Feel free to whisper your
{"\n"}little secrets.
</h1>
</div>
);
}
@@ -1,20 +1,12 @@
.logo {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
color: var(--color-text-primary);
display: inline-flex;
z-index: 2;
}
.brand {
font-family: var(--font-athelas);
font-size: var(--font-size-title-x-large);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1;
}
.tagline {
font-size: var(--font-size-md);
color: var(--color-text-secondary);
.image {
display: block;
width: auto;
height: auto;
/* Apple 平台 Athelas 渲染时仍可能需要 fallback */
font-family: var(--font-athelas, serif);
}
+12 -5
View File
@@ -3,17 +3,24 @@
* Splash Logo
*
* 原始 Dart: lib/ui/splash/widgets/splash_logo.dart
*
* 当前实现:纯文字 logo"cozsweet" + tagline)。
* 资源:`Assets.images.icLogoHome`(待替换为 `<Image src="/splash/logo.svg" />`)。
* 资源: /public/images/splash/ic-logo-home.png (kebab-case 重命名后)
* 原名: ic_logo_home.png (snake_case)
* 渲染: Assets.images.icLogoHome.image() → next/image <Image>
*/
import Image from "next/image";
import styles from "./splash-logo.module.css";
export function SplashLogo() {
return (
<div className={styles.logo}>
<span className={styles.brand}>cozsweet</span>
<span className={styles.tagline}>Your exclusive AI boyfriend</span>
<Image
src="/images/splash/ic-logo-home.png"
alt="cozsweet"
width={120}
height={40}
priority
className={styles.image}
/>
</div>
);
}
@@ -8,6 +8,7 @@
overflow: hidden;
}
/* 渐变:accent (bottom-left) → transparent (center-right) */
.gradientOverlay {
position: absolute;
inset: 0;
@@ -16,7 +17,7 @@
var(--color-accent),
transparent 60%
);
opacity: 0.4;
opacity: 0.5;
z-index: 1;
pointer-events: none;
}
@@ -40,7 +41,9 @@
.bottom {
margin: var(--spacing-xxxl) 0 0 0;
font-family: var(--font-athelas);
font-size: var(--font-size-md);
font-weight: 400;
line-height: 1.5;
color: rgba(255, 255, 255, 0.9);
text-align: center;
+7 -1
View File
@@ -42,13 +42,19 @@ export function SplashScreen() {
<MobileShell background="var(--color-sidebar-background)">
<div className={styles.wrapper}>
<SplashBackground />
{/* 渐变叠加层:accent → transparent (bottom-left → center-right) */}
<div className={styles.gradientOverlay} aria-hidden="true" />
<div className={styles.content}>
<SplashLogo />
<div className={styles.spacer} />
<SplashContent />
<div className={styles.buttonArea}>
<SplashButton onSkip={() => router.push(ROUTES.chat)} />
<SplashButton
onSkip={() => router.push(ROUTES.chat)}
onFacebookLogin={() => {
/* Facebook 登录触发由 SplashButton 内部 dispatch */
}}
/>
</div>
<p className={styles.bottom}>
Elio Silvestri, Your exclusive AI boyfriend