feat(splash): simplify multi-character home layout
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("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'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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}'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 {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,26 +82,23 @@ export function SplashScreen() {
|
|||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<SplashLogo />
|
<SplashLogo />
|
||||||
<div className={styles.spacer} />
|
<div className={styles.spacer} />
|
||||||
<SplashLatestMessage
|
<div className={styles.bottomStack}>
|
||||||
message={latestMessage.message}
|
{hasLatestMessage ? (
|
||||||
characterName={character.shortName}
|
<SplashLatestMessage
|
||||||
avatarUrl={character.assets.avatar}
|
message={latestMessage.message}
|
||||||
isLoading={latestMessage.isLoading}
|
characterName={character.shortName}
|
||||||
onOpenChat={handleStartChat}
|
avatarUrl={character.assets.avatar}
|
||||||
/>
|
isLoading={latestMessage.isLoading}
|
||||||
<SplashContent characterName={character.shortName} />
|
onOpenChat={handleStartChat}
|
||||||
<div className={styles.buttonArea}>
|
/>
|
||||||
|
) : null}
|
||||||
|
<SplashContent characterName={character.shortName} />
|
||||||
<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",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user