From 2b76e7f34ccff695087ff44fcaa809f896765add Mon Sep 17 00:00:00 2001
From: Harkamal Randhawa
Date: Sun, 24 May 2026 05:15:37 -0600
Subject: [PATCH] Scale landing pet preview for small viewports.
ResizeObserver on the landing LCD drives pet sprite scale while capping LCD height so the hero preview stays proportional on mobile.
---
app/globals.css | 6 +++
components/landing/landing-pet-preview.tsx | 44 +++++++++++++++++++++-
components/pet/animated-pet-sprite.tsx | 4 +-
components/pet/pet-habitat.tsx | 14 +++++--
4 files changed, 62 insertions(+), 6 deletions(-)
diff --git a/app/globals.css b/app/globals.css
index 9b9551f..7e082c4 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -116,6 +116,12 @@
max-width: calc(32rem - 2.5rem - 2rem);
}
+ /* Landing hero preview: cap LCD height on narrow viewports */
+ .tamagotchi-lcd-landing {
+ @apply aspect-[4/3] w-full shrink-0;
+ max-height: clamp(140px, 42vw, 280px);
+ }
+
.pet-viewport-stage {
@apply flex min-h-0 w-full flex-1 flex-col items-center justify-center;
}
diff --git a/components/landing/landing-pet-preview.tsx b/components/landing/landing-pet-preview.tsx
index bff1178..78277c0 100644
--- a/components/landing/landing-pet-preview.tsx
+++ b/components/landing/landing-pet-preview.tsx
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import { PetHabitat } from "@/components/pet/pet-habitat";
import {
@@ -23,18 +23,49 @@ function LandingPetPreviewSkeleton() {
-
+
);
}
+function computeLandingPetScale(lcdWidth: number): number {
+ return Math.min(7, Math.max(4, Math.round(lcdWidth / 36)));
+}
+
export function LandingPetPreview() {
+ const resizeObserverRef = useRef(null);
const [isLoading, setIsLoading] = useState(true);
const [isSignedIn, setIsSignedIn] = useState(false);
+ const [petScale, setPetScale] = useState(7);
const [customization, setCustomization] = useState(
defaultAvatarCustomization,
);
+ const lcdRef = useCallback((node: HTMLDivElement | null) => {
+ resizeObserverRef.current?.disconnect();
+ resizeObserverRef.current = null;
+
+ if (!node) {
+ return;
+ }
+
+ const updateScale = (width: number) => {
+ setPetScale(computeLandingPetScale(width));
+ };
+
+ updateScale(node.getBoundingClientRect().width);
+
+ const observer = new ResizeObserver((entries) => {
+ const entry = entries[0];
+ if (entry) {
+ updateScale(entry.contentRect.width);
+ }
+ });
+
+ observer.observe(node);
+ resizeObserverRef.current = observer;
+ }, []);
+
const loadPreview = useCallback(async () => {
setIsLoading(true);
@@ -65,6 +96,12 @@ export function LandingPetPreview() {
void loadPreview();
}, [loadPreview]);
+ useEffect(() => {
+ return () => {
+ resizeObserverRef.current?.disconnect();
+ };
+ }, []);
+
if (isLoading) {
return ;
}
@@ -74,6 +111,9 @@ export function LandingPetPreview() {
customization={customization}
className="w-full max-w-sm"
showStyleLink={false}
+ petScale={petScale}
+ lcdClassName="tamagotchi-lcd-landing"
+ lcdRef={lcdRef}
/>
);
diff --git a/components/pet/animated-pet-sprite.tsx b/components/pet/animated-pet-sprite.tsx
index 620701d..95da511 100644
--- a/components/pet/animated-pet-sprite.tsx
+++ b/components/pet/animated-pet-sprite.tsx
@@ -13,6 +13,7 @@ import { cn } from "@/lib/utils";
type AnimatedPetSpriteProps = {
customization: AvatarCustomization;
className?: string;
+ petScale?: number;
};
type WanderPosition = {
@@ -52,6 +53,7 @@ function randomPosition(current?: WanderPosition): WanderPosition {
export function AnimatedPetSprite({
customization,
className,
+ petScale = 7,
}: AnimatedPetSpriteProps) {
const [position, setPosition] = useState({
x: PET_WANDER_START.x,
@@ -98,7 +100,7 @@ export function AnimatedPetSprite({
colors={customization.colors}
variants={customization.variants}
equippedItems={customization.equippedItems}
- scale={7}
+ scale={petScale}
compact
className="!h-auto max-h-none"
/>
diff --git a/components/pet/pet-habitat.tsx b/components/pet/pet-habitat.tsx
index 2867d91..d3a3809 100644
--- a/components/pet/pet-habitat.tsx
+++ b/components/pet/pet-habitat.tsx
@@ -23,6 +23,9 @@ type PetHabitatProps = {
className?: string;
fillViewport?: boolean;
showStyleLink?: boolean;
+ petScale?: number;
+ lcdClassName?: string;
+ lcdRef?: React.Ref;
};
const MOOD_LABELS = {
@@ -36,6 +39,9 @@ export function PetHabitat({
className,
fillViewport = false,
showStyleLink = true,
+ petScale,
+ lcdClassName: lcdClassNameOverride,
+ lcdRef,
}: PetHabitatProps) {
const [condition, setCondition] = useState(
defaultAvatarCondition,
@@ -60,7 +66,9 @@ export function PetHabitat({
const lcdClassName = cn(
"tamagotchi-lcd relative",
- fillViewport ? "tamagotchi-lcd-pet-fill" : "tamagotchi-lcd-pet-match",
+ fillViewport
+ ? "tamagotchi-lcd-pet-fill"
+ : (lcdClassNameOverride ?? "tamagotchi-lcd-pet-match"),
);
if (!isReady) {
@@ -82,9 +90,9 @@ export function PetHabitat({
-