feat(splash): simplify multi-character home layout

This commit is contained in:
Codex
2026-07-23 13:27:16 +08:00
parent 3c7b0c30e0
commit b1f52c68e8
10 changed files with 67 additions and 65 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

@@ -12,7 +12,7 @@ describe("splash Tailwind components", () => {
expect(html).toContain("z-2"); expect(html).toContain("z-2");
expect(html).toContain("inline-flex"); expect(html).toContain("inline-flex");
expect(html).toContain("block h-auto w-auto"); expect(html).toContain("block h-auto w-[clamp(136px,38vw,158px)]");
expect(html).toContain("%2Fimages%2Fsplash%2Fic-logo-home.png"); expect(html).toContain("%2Fimages%2Fsplash%2Fic-logo-home.png");
}); });
@@ -30,10 +30,9 @@ describe("splash Tailwind components", () => {
it("renders SplashContent with Tailwind typography classes", () => { it("renders SplashContent with Tailwind typography classes", () => {
const html = renderToStaticMarkup(<SplashContent characterName="Maya" />); const html = renderToStaticMarkup(<SplashContent characterName="Maya" />);
expect(html).toContain("flex flex-col gap-md"); expect(html).toContain("Welcome to Maya&#x27;s private world.");
expect(html).toContain("Just you and me.");
expect(html).toContain("font-(family-name:--font-athelas)"); expect(html).toContain("font-(family-name:--font-athelas)");
expect(html).toContain("whitespace-pre-line");
expect(html).toContain("Welcome to Maya");
}); });
it("renders SplashButton as an always-ready action", () => { it("renders SplashButton as an always-ready action", () => {
@@ -12,8 +12,10 @@ import Image from "next/image";
export function SplashBackground({ export function SplashBackground({
src, src,
objectPosition,
}: { }: {
src: string; src: string;
objectPosition?: string;
}) { }) {
return ( return (
<div className="absolute inset-0 z-0 overflow-hidden bg-splash-background"> <div className="absolute inset-0 z-0 overflow-hidden bg-splash-background">
@@ -24,6 +26,7 @@ export function SplashBackground({
priority priority
sizes="(max-width: 540px) 100vw, 540px" sizes="(max-width: 540px) 100vw, 540px"
className="object-cover object-center" className="object-cover object-center"
style={{ objectPosition }}
/> />
</div> </div>
); );
+6 -8
View File
@@ -2,17 +2,15 @@
* Splash 主内容文案 * Splash 主内容文案
*/ */
const APOSTROPHE = "'";
export function SplashContent({ characterName }: { characterName: string }) { export function SplashContent({ characterName }: { characterName: string }) {
return ( return (
<div className="z-2 flex flex-col gap-md"> <div className="z-2 flex flex-col gap-1.5 text-left text-white">
<h1 className="m-0 whitespace-pre-line text-left font-(family-name:--font-athelas) text-[clamp(28px,7.8vw,30px)] font-bold italic leading-[1.28] text-white"> <h1 className="m-0 max-w-105 font-(family-name:--font-athelas) text-[clamp(29px,8.2vw,35px)] font-bold leading-[1.08] text-white [text-shadow:0_2px_18px_rgba(0,0,0,0.42)]">
Welcome to {characterName}{APOSTROPHE}s secret hideout~ Welcome to {characterName}&apos;s private world.
{"\n"}It{APOSTROPHE}s just the two of us now.
{"\n"}Feel free to whisper your
{"\n"}little secrets.
</h1> </h1>
<p className="m-0 text-[clamp(15px,4.1vw,18px)] font-semibold leading-[1.4] text-white/92 [text-shadow:0_2px_12px_rgba(0,0,0,0.5)]">
Just you and me.
</p>
</div> </div>
); );
} }
@@ -1,16 +1,15 @@
.card { .card {
display: grid; display: grid;
box-sizing: border-box;
width: 100%; width: 100%;
height: 80px;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
align-items: center; align-items: center;
gap: 12px; gap: 10px;
margin-top: var(--page-section-gap-lg, 26px); padding: 9px 11px;
padding: 12px 14px; border: 1px solid rgba(255, 255, 255, 0.28);
border: 1px solid rgba(255, 255, 255, 0.22); border-radius: 16px;
border-radius: 24px; background: rgba(31, 22, 29, 0.48);
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.24), rgba(255, 255, 255, 0.1)),
rgba(28, 19, 30, 0.18);
color: #ffffff; color: #ffffff;
cursor: pointer; cursor: pointer;
font: inherit; font: inherit;
@@ -19,8 +18,8 @@
touch-action: manipulation; touch-action: manipulation;
box-shadow: box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.18),
0 18px 42px rgba(46, 22, 38, 0.18); 0 12px 30px rgba(31, 17, 27, 0.2);
backdrop-filter: blur(18px); backdrop-filter: blur(14px);
animation: messageFloatIn 0.5s ease 0.08s backwards; animation: messageFloatIn 0.5s ease 0.08s backwards;
transition: transition:
border-color 0.18s ease, border-color 0.18s ease,
@@ -53,8 +52,8 @@
.avatarWrap { .avatarWrap {
display: grid; display: grid;
width: 48px; width: 40px;
height: 48px; height: 40px;
place-items: center; place-items: center;
border: 2px solid rgba(255, 255, 255, 0.74); border: 2px solid rgba(255, 255, 255, 0.74);
border-radius: 9999px; border-radius: 9999px;
@@ -92,10 +91,15 @@
} }
.message { .message {
display: -webkit-box;
min-height: 2.6em;
overflow: hidden;
color: #ffffff; color: #ffffff;
font-size: clamp(14px, 3.333vw, 17px); font-size: clamp(13px, 3.45vw, 16px);
font-weight: 760; font-weight: 720;
line-height: 1.35; line-height: 1.3;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
} }
.messageLoading { .messageLoading {
+1 -1
View File
@@ -9,7 +9,7 @@ export function SplashLogo() {
width={180} width={180}
height={60} height={60}
priority priority
className="block h-auto w-auto" className="block h-auto w-[clamp(136px,38vw,158px)]"
/> />
</div> </div>
); );
@@ -8,16 +8,13 @@
overflow: hidden; overflow: hidden;
} }
/* 渐变:accent (bottom-left) → transparent (center-right) */ .imageScrim {
.gradientOverlay {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: linear-gradient( background: rgba(25, 17, 23, 0.08);
to top right, box-shadow:
var(--color-accent), inset 0 -290px 150px -145px rgba(20, 12, 18, 0.78),
transparent 60% inset 0 110px 90px -90px rgba(11, 8, 10, 0.42);
);
opacity: 0.5;
z-index: 1; z-index: 1;
pointer-events: none; pointer-events: none;
} }
@@ -49,15 +46,9 @@
flex: 1 1 auto; flex: 1 1 auto;
} }
.buttonArea { .bottomStack {
margin-top: var(--page-section-gap-lg, 26px); display: flex;
} flex-direction: column;
gap: clamp(12px, 3.6vw, 18px);
.bottom { padding-bottom: clamp(12px, 3.6vw, 18px);
margin: var(--page-section-gap-lg, 32px) 0 0 0;
font-size: var(--responsive-body, var(--font-size-md));
font-weight: 400;
line-height: 1.5;
color: rgba(255, 255, 255, 0.9);
text-align: left;
} }
+12 -10
View File
@@ -62,12 +62,17 @@ export function SplashScreen() {
pwaUtil.prepareInstallPrompt(); pwaUtil.prepareInstallPrompt();
}, []); }, []);
const hasLatestMessage =
latestMessage.isLoading || Boolean(latestMessage.message);
return ( return (
<MobileShell background="var(--color-splash-background)"> <MobileShell background="var(--color-splash-background)">
<div className={styles.wrapper}> <div className={styles.wrapper}>
<SplashBackground src={character.assets.cover} /> <SplashBackground
{/* 渐变叠加层:accent → transparent (bottom-left → center-right) */} src={character.assets.splashCover ?? character.assets.cover}
<div className={styles.gradientOverlay} aria-hidden="true" /> objectPosition={character.id === "maya-tan" ? "52% center" : undefined}
/>
<div className={styles.imageScrim} aria-hidden="true" />
<div className={styles.favoriteAction}> <div className={styles.favoriteAction}>
<FavoriteEntryButton <FavoriteEntryButton
characterSlug={character.slug} characterSlug={character.slug}
@@ -77,6 +82,8 @@ export function SplashScreen() {
<div className={styles.content}> <div className={styles.content}>
<SplashLogo /> <SplashLogo />
<div className={styles.spacer} /> <div className={styles.spacer} />
<div className={styles.bottomStack}>
{hasLatestMessage ? (
<SplashLatestMessage <SplashLatestMessage
message={latestMessage.message} message={latestMessage.message}
characterName={character.shortName} characterName={character.shortName}
@@ -84,19 +91,14 @@ export function SplashScreen() {
isLoading={latestMessage.isLoading} isLoading={latestMessage.isLoading}
onOpenChat={handleStartChat} onOpenChat={handleStartChat}
/> />
) : null}
<SplashContent characterName={character.shortName} /> <SplashContent characterName={character.shortName} />
<div className={styles.buttonArea}>
<SplashButton onStartChat={handleStartChat} /> <SplashButton onStartChat={handleStartChat} />
</div> </div>
<p className={styles.bottom}>
{character.displayName}, {character.tagline}
<br />
24/7 online | Chat | Companion | Heal | Sweet moments
</p>
</div> </div>
<AppBottomNav <AppBottomNav
activeItem="chat" activeItem="chat"
privateZoneLabel={character.copy.privateZoneTitle} privateZoneLabel="Private Zone"
onChatClick={handleOpenSplash} onChatClick={handleOpenSplash}
onPrivateZoneClick={handleOpenPrivateZone} onPrivateZoneClick={handleOpenPrivateZone}
onMenuClick={handleOpenMenu} onMenuClick={handleOpenMenu}
@@ -90,6 +90,9 @@ describe("local character catalog", () => {
it("resolves characters by id and normalized slug", () => { it("resolves characters by id and normalized slug", () => {
expect(getCharacterById("maya-tan")?.displayName).toBe("Maya Tan"); expect(getCharacterById("maya-tan")?.displayName).toBe("Maya Tan");
expect(getCharacterById("maya-tan")?.assets.splashCover).toBe(
"/images/cover/maya-home.webp",
);
expect(getCharacterBySlug(" NAYELI ")?.displayName).toBe( expect(getCharacterBySlug(" NAYELI ")?.displayName).toBe(
"Nayeli Cervantes", "Nayeli Cervantes",
); );
+2
View File
@@ -16,6 +16,7 @@ export interface CharacterProfile {
readonly assets: { readonly assets: {
readonly avatar: string; readonly avatar: string;
readonly cover: string; readonly cover: string;
readonly splashCover?: string;
readonly chatBackground: string; readonly chatBackground: string;
readonly privateZoneBanner: string; readonly privateZoneBanner: string;
}; };
@@ -97,6 +98,7 @@ const CHARACTER_PROFILES: readonly CharacterProfile[] = Object.freeze([
assets: { assets: {
avatar: "/images/avatar/maya.png", avatar: "/images/avatar/maya.png",
cover: "/images/cover/maya.webp", cover: "/images/cover/maya.webp",
splashCover: "/images/cover/maya-home.webp",
chatBackground: "/images/chat/bg-chatpage.png", chatBackground: "/images/chat/bg-chatpage.png",
privateZoneBanner: "/images/private-zone/banner/maya.png", privateZoneBanner: "/images/private-zone/banner/maya.png",
}, },