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