feat(assets): add initial fonts and image assets
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 267 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 704 KiB |
@@ -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 JS(auth + 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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 = "'";
|
||||
|
||||
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'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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||