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 type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import localFont from "next/font/local";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
@@ -15,6 +16,39 @@ const geistMono = Geist_Mono({
|
|||||||
subsets: ["latin"],
|
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 = {
|
export const metadata: Metadata = {
|
||||||
title: "cozsweet",
|
title: "cozsweet",
|
||||||
description: "cozsweet frontend (Next.js)",
|
description: "cozsweet frontend (Next.js)",
|
||||||
@@ -31,7 +65,7 @@ export default function RootLayout({
|
|||||||
<html
|
<html
|
||||||
lang="en"
|
lang="en"
|
||||||
suppressHydrationWarning
|
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">
|
<body className="min-h-full flex flex-col">
|
||||||
{/* Facebook SDK JS(auth + splash 入口会用到 FB.login) */}
|
{/* Facebook SDK JS(auth + splash 入口会用到 FB.login) */}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
.bg {
|
.bg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
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;
|
z-index: 0;
|
||||||
|
background: var(--color-sidebar-background);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
/**
|
import Image from "next/image";
|
||||||
* Splash 背景
|
|
||||||
*
|
|
||||||
* 原始 Dart: lib/ui/splash/widgets/splash_background.dart
|
|
||||||
* 使用 `Assets.images.picBgHome.image(fit: BoxFit.cover)` 渲染全屏背景图。
|
|
||||||
*
|
|
||||||
* 当前实现:纯色 + CSS 渐变占位(待 `public/splash/bg.png` 资源到位后可切换为 `<Image>`)。
|
|
||||||
*/
|
|
||||||
import styles from "./splash-background.module.css";
|
import styles from "./splash-background.module.css";
|
||||||
|
|
||||||
export function SplashBackground() {
|
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 {
|
.wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
gap: var(--spacing-md);
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-26);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 2;
|
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";
|
"use client";
|
||||||
/**
|
/**
|
||||||
* Splash 底部按钮组(Skip + Facebook 登录)
|
* Splash 底部按钮组
|
||||||
*
|
*
|
||||||
* 原始 Dart: lib/ui/splash/widgets/splash_button.dart
|
* 原始 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 { useAuthState, useAuthDispatch } from "@/contexts/auth/auth-context";
|
||||||
|
import { LoadingIndicator } from "@/app/_components/core/loading-indicator";
|
||||||
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 styles from "./splash-button.module.css";
|
import styles from "./splash-button.module.css";
|
||||||
|
|
||||||
export interface SplashButtonProps {
|
export interface SplashButtonProps {
|
||||||
onSkip: () => void;
|
onSkip: () => void;
|
||||||
|
onFacebookLogin: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SplashButton({ onSkip }: SplashButtonProps) {
|
export function SplashButton({
|
||||||
|
onSkip,
|
||||||
|
onFacebookLogin,
|
||||||
|
}: SplashButtonProps) {
|
||||||
const state = useAuthState();
|
const state = useAuthState();
|
||||||
const dispatch = useAuthDispatch();
|
const dispatch = useAuthDispatch();
|
||||||
|
const isLoading = state.isLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<AuthSocialButtons
|
{/* Skip 文本按钮 */}
|
||||||
loadingProvider={state.isLoading ? "facebook" : null}
|
<button
|
||||||
onFacebook={() => dispatch({ type: "AuthFacebookLoginSubmitted" })}
|
type="button"
|
||||||
onGoogle={() => dispatch({ type: "AuthGoogleLoginSubmitted" })}
|
onClick={onSkip}
|
||||||
/>
|
className={styles.skip}
|
||||||
<AuthDivider label="or" />
|
disabled={isLoading}
|
||||||
<AuthPrimaryButton onClick={onSkip} variant="primary">
|
>
|
||||||
Skip — Continue as Guest
|
Skip
|
||||||
</AuthPrimaryButton>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,17 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-md);
|
gap: var(--spacing-md);
|
||||||
color: var(--color-text-primary);
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin: 0;
|
|
||||||
font-family: var(--font-athelas);
|
font-family: var(--font-athelas);
|
||||||
font-size: var(--font-size-26);
|
font-size: var(--font-size-title);
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
line-height: 1.3;
|
font-style: italic;
|
||||||
}
|
line-height: 1.4;
|
||||||
|
color: #ffffff;
|
||||||
.subtitle {
|
text-align: left;
|
||||||
|
white-space: pre-line;
|
||||||
margin: 0;
|
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";
|
"use client";
|
||||||
/**
|
/**
|
||||||
* Splash 内容文案
|
* Splash 主内容文案
|
||||||
*
|
*
|
||||||
* 原始 Dart: lib/ui/splash/widgets/splash_content.dart
|
* 原始 Dart: lib/ui/splash/widgets/splash_content.dart
|
||||||
|
*
|
||||||
|
* 关键差异:
|
||||||
|
* - 使用 Athelas 字体(衬线体)
|
||||||
|
* - 标题加粗 + 斜体(与原 Dart 一致)
|
||||||
|
* - 颜色:白色(背景图上需高对比度)
|
||||||
*/
|
*/
|
||||||
import styles from "./splash-content.module.css";
|
import styles from "./splash-content.module.css";
|
||||||
|
|
||||||
|
const APOSTROPHE = "'";
|
||||||
|
|
||||||
export function SplashContent() {
|
export function SplashContent() {
|
||||||
return (
|
return (
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<h1 className={styles.title}>Chat with Elio, anytime, anywhere</h1>
|
<h1 className={styles.title}>
|
||||||
<p className={styles.subtitle}>
|
Welcome to my secret hideout~
|
||||||
A safe, private space to talk, share, and unwind — your AI companion who's always there.
|
{"\n"}It{APOSTROPHE}s just the two of us now.
|
||||||
</p>
|
{"\n"}Feel free to whisper your
|
||||||
|
{"\n"}little secrets.
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
.logo {
|
.logo {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-xs);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand {
|
.image {
|
||||||
font-family: var(--font-athelas);
|
display: block;
|
||||||
font-size: var(--font-size-title-x-large);
|
width: auto;
|
||||||
font-weight: 700;
|
height: auto;
|
||||||
letter-spacing: -0.02em;
|
/* Apple 平台 Athelas 渲染时仍可能需要 fallback */
|
||||||
line-height: 1;
|
font-family: var(--font-athelas, serif);
|
||||||
}
|
|
||||||
|
|
||||||
.tagline {
|
|
||||||
font-size: var(--font-size-md);
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,24 @@
|
|||||||
* Splash Logo
|
* Splash Logo
|
||||||
*
|
*
|
||||||
* 原始 Dart: lib/ui/splash/widgets/splash_logo.dart
|
* 原始 Dart: lib/ui/splash/widgets/splash_logo.dart
|
||||||
*
|
* 资源: /public/images/splash/ic-logo-home.png (kebab-case 重命名后)
|
||||||
* 当前实现:纯文字 logo("cozsweet" + tagline)。
|
* 原名: ic_logo_home.png (snake_case)
|
||||||
* 资源:`Assets.images.icLogoHome`(待替换为 `<Image src="/splash/logo.svg" />`)。
|
* 渲染: Assets.images.icLogoHome.image() → next/image <Image>
|
||||||
*/
|
*/
|
||||||
|
import Image from "next/image";
|
||||||
import styles from "./splash-logo.module.css";
|
import styles from "./splash-logo.module.css";
|
||||||
|
|
||||||
export function SplashLogo() {
|
export function SplashLogo() {
|
||||||
return (
|
return (
|
||||||
<div className={styles.logo}>
|
<div className={styles.logo}>
|
||||||
<span className={styles.brand}>cozsweet</span>
|
<Image
|
||||||
<span className={styles.tagline}>Your exclusive AI boyfriend</span>
|
src="/images/splash/ic-logo-home.png"
|
||||||
|
alt="cozsweet"
|
||||||
|
width={120}
|
||||||
|
height={40}
|
||||||
|
priority
|
||||||
|
className={styles.image}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 渐变:accent (bottom-left) → transparent (center-right) */
|
||||||
.gradientOverlay {
|
.gradientOverlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -16,7 +17,7 @@
|
|||||||
var(--color-accent),
|
var(--color-accent),
|
||||||
transparent 60%
|
transparent 60%
|
||||||
);
|
);
|
||||||
opacity: 0.4;
|
opacity: 0.5;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -40,7 +41,9 @@
|
|||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
margin: var(--spacing-xxxl) 0 0 0;
|
margin: var(--spacing-xxxl) 0 0 0;
|
||||||
|
font-family: var(--font-athelas);
|
||||||
font-size: var(--font-size-md);
|
font-size: var(--font-size-md);
|
||||||
|
font-weight: 400;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -42,13 +42,19 @@ export function SplashScreen() {
|
|||||||
<MobileShell background="var(--color-sidebar-background)">
|
<MobileShell background="var(--color-sidebar-background)">
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<SplashBackground />
|
<SplashBackground />
|
||||||
|
{/* 渐变叠加层:accent → transparent (bottom-left → center-right) */}
|
||||||
<div className={styles.gradientOverlay} aria-hidden="true" />
|
<div className={styles.gradientOverlay} aria-hidden="true" />
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<SplashLogo />
|
<SplashLogo />
|
||||||
<div className={styles.spacer} />
|
<div className={styles.spacer} />
|
||||||
<SplashContent />
|
<SplashContent />
|
||||||
<div className={styles.buttonArea}>
|
<div className={styles.buttonArea}>
|
||||||
<SplashButton onSkip={() => router.push(ROUTES.chat)} />
|
<SplashButton
|
||||||
|
onSkip={() => router.push(ROUTES.chat)}
|
||||||
|
onFacebookLogin={() => {
|
||||||
|
/* Facebook 登录触发由 SplashButton 内部 dispatch */
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className={styles.bottom}>
|
<p className={styles.bottom}>
|
||||||
Elio Silvestri, Your exclusive AI boyfriend
|
Elio Silvestri, Your exclusive AI boyfriend
|
||||||
|
|||||||