Add inline pet rename, larger header text, and HaBit branding.

Let users edit the pet name from PetHabitat with a pencil icon, bump the name and mood header sizing, and rename user-facing Habit Pet copy to HaBit Pet.
This commit is contained in:
2026-05-24 08:47:48 -06:00
parent d35b4e57d9
commit 3be729327b
14 changed files with 201 additions and 18 deletions

View File

@@ -6,7 +6,7 @@ import { AppShell } from "@/components/layout/app-shell";
export default function AvatarPage() { export default function AvatarPage() {
return ( return (
<AppShell <AppShell
title="Your Habit Pet" title="Your HaBit Pet"
description="Watch your pet roam, breathe, and react to how you care for them." description="Watch your pet roam, breathe, and react to how you care for them."
fillViewport fillViewport
centered centered

View File

@@ -22,7 +22,7 @@ export default function Page() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Your account is ready. You can sign in and start using Habit Pet. Your account is ready. You can sign in and start using HaBit Pet.
</p> </p>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -12,7 +12,7 @@ const defaultUrl = process.env.VERCEL_URL
export const metadata: Metadata = { export const metadata: Metadata = {
metadataBase: new URL(defaultUrl), metadataBase: new URL(defaultUrl),
title: "Habit Pet", title: "HaBit Pet",
description: "Build habits, care for your pixel pet, and track wellness every day.", description: "Build habits, care for your pixel pet, and track wellness every day.",
}; };

View File

@@ -45,5 +45,13 @@ export function AvatarPageContent() {
); );
} }
return <PetHabitat customization={customization} fillViewport />; return (
<PetHabitat
customization={customization}
fillViewport
onNameChange={(name) =>
setCustomization((current) => ({ ...current, name }))
}
/>
);
} }

View File

@@ -131,6 +131,10 @@ export function LandingPetPreview() {
customization={customization} customization={customization}
className="w-full max-w-sm" className="w-full max-w-sm"
showStyleLink={false} showStyleLink={false}
nameEditable={isSignedIn}
onNameChange={(name) =>
setCustomization((current) => ({ ...current, name }))
}
petScale={petScale} petScale={petScale}
lcdClassName="tamagotchi-lcd-landing" lcdClassName="tamagotchi-lcd-landing"
lcdRef={lcdRef} lcdRef={lcdRef}

View File

@@ -41,7 +41,7 @@ export function HabitBrand({
/> />
) : ( ) : (
<span <span
className="font-pixel select-none text-[10px] uppercase tracking-widest text-primary" className="font-pixel select-none text-[10px] tracking-wide text-primary"
aria-hidden="true" aria-hidden="true"
> >
HaBit HaBit

View File

@@ -30,7 +30,7 @@ export function OnboardingCustomizeForm() {
}, },
{ completeOnboarding: true }, { completeOnboarding: true },
); );
toast("Avatar confirmed. Welcome to Habit Pet!", "success"); toast("Avatar confirmed. Welcome to HaBit Pet!", "success");
router.push(routes.avatar); router.push(routes.avatar);
router.refresh(); router.refresh();
} catch (saveError) { } catch (saveError) {
@@ -49,7 +49,7 @@ export function OnboardingCustomizeForm() {
return ( return (
<AvatarCustomizeLayout <AvatarCustomizeLayout
eyebrow="Step 2 of 2" eyebrow="Step 2 of 2"
title="Create your Habit Pet" title="Create your HaBit Pet"
description="Customize your pet's look, give them a name, and confirm before entering the app." description="Customize your pet's look, give them a name, and confirm before entering the app."
> >
<CharacterCreator <CharacterCreator

View File

@@ -54,7 +54,7 @@ export function OnboardingQuizForm() {
Step 1 of 2 Step 1 of 2
</p> </p>
<h1 className="mt-2 text-2xl font-semibold tracking-tight"> <h1 className="mt-2 text-2xl font-semibold tracking-tight">
Welcome to Habit Pet Welcome to HaBit Pet
</h1> </h1>
<p className="mt-2 text-sm text-muted-foreground"> <p className="mt-2 text-sm text-muted-foreground">
Choose a focus area, then design your pet in the next step. Choose a focus area, then design your pet in the next step.

View File

@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import { AnimatedPetSprite } from "@/components/pet/animated-pet-sprite"; import { AnimatedPetSprite } from "@/components/pet/animated-pet-sprite";
import { ParallaxRoomBackground } from "@/components/pet/parallax-room-background"; import { ParallaxRoomBackground } from "@/components/pet/parallax-room-background";
import { PetNameHeader } from "@/components/pet/pet-name-header";
import { PetStyleLink } from "@/components/pet/pet-style-link"; import { PetStyleLink } from "@/components/pet/pet-style-link";
import { PixelGauge } from "@/components/pet/pixel-gauge"; import { PixelGauge } from "@/components/pet/pixel-gauge";
import type { AvatarCustomization } from "@/lib/avatar-customization-storage"; import type { AvatarCustomization } from "@/lib/avatar-customization-storage";
@@ -23,6 +24,8 @@ type PetHabitatProps = {
className?: string; className?: string;
fillViewport?: boolean; fillViewport?: boolean;
showStyleLink?: boolean; showStyleLink?: boolean;
nameEditable?: boolean;
onNameChange?: (name: string) => void;
petScale?: number; petScale?: number;
lcdClassName?: string; lcdClassName?: string;
lcdRef?: React.Ref<HTMLDivElement>; lcdRef?: React.Ref<HTMLDivElement>;
@@ -39,6 +42,8 @@ export function PetHabitat({
className, className,
fillViewport = false, fillViewport = false,
showStyleLink = true, showStyleLink = true,
nameEditable = true,
onNameChange,
petScale, petScale,
lcdClassName: lcdClassNameOverride, lcdClassName: lcdClassNameOverride,
lcdRef, lcdRef,
@@ -83,12 +88,12 @@ export function PetHabitat({
return ( return (
<div className={shellClassName}> <div className={shellClassName}>
<div className="mb-3 shrink-0 border-b-2 border-border/60 pb-3"> <PetNameHeader
<p className="text-sm text-foreground">{customization.name}</p> name={customization.name}
<p className="mt-1 text-[9px] uppercase tracking-[0.25em] text-secondary"> moodLabel={MOOD_LABELS[condition.mood]}
{MOOD_LABELS[condition.mood]} editable={nameEditable}
</p> onNameChange={onNameChange}
</div> />
<div ref={lcdRef} className={lcdClassName}> <div ref={lcdRef} className={lcdClassName}>
<ParallaxRoomBackground roomId={roomId as RoomBackgroundId} /> <ParallaxRoomBackground roomId={roomId as RoomBackgroundId} />

View File

@@ -0,0 +1,166 @@
"use client";
import { Pencil } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Input } from "@/components/ui/input";
import {
DEFAULT_AVATAR_NAME,
saveAvatarName,
} from "@/lib/avatar-customization-storage";
import { cn } from "@/lib/utils";
type PetNameHeaderProps = {
name: string;
moodLabel: string;
editable?: boolean;
onNameChange?: (name: string) => void;
className?: string;
};
export function PetNameHeader({
name,
moodLabel,
editable = true,
onNameChange,
className,
}: PetNameHeaderProps) {
const [displayName, setDisplayName] = useState(name);
const [isEditing, setIsEditing] = useState(false);
const [draft, setDraft] = useState(name);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
setDisplayName(name);
if (!isEditing) {
setDraft(name);
}
}, [name, isEditing]);
useEffect(() => {
if (isEditing) {
inputRef.current?.focus();
inputRef.current?.select();
}
}, [isEditing]);
const startEditing = () => {
if (!editable || isSaving) {
return;
}
setDraft(displayName);
setError(null);
setIsEditing(true);
};
const cancelEditing = () => {
setDraft(displayName);
setError(null);
setIsEditing(false);
};
const commitName = async () => {
const trimmed = draft.trim() || DEFAULT_AVATAR_NAME;
if (trimmed === displayName) {
setIsEditing(false);
return;
}
if (!editable) {
setIsEditing(false);
return;
}
setIsSaving(true);
setError(null);
try {
const saved = await saveAvatarName(trimmed);
setDisplayName(saved);
onNameChange?.(saved);
setIsEditing(false);
} catch (commitError) {
setError(
commitError instanceof Error
? commitError.message
: "Could not save pet name.",
);
setDraft(displayName);
} finally {
setIsSaving(false);
}
};
return (
<div
className={cn("mb-3 shrink-0 border-b-2 border-border/60 pb-3", className)}
onClick={(event) => {
if (isEditing) {
event.stopPropagation();
}
}}
>
{isEditing ? (
<div className="space-y-2">
<Input
ref={inputRef}
value={draft}
maxLength={32}
disabled={isSaving}
aria-label="Pet name"
className="h-9 font-pixel text-base"
onChange={(event) => setDraft(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
void commitName();
}
if (event.key === "Escape") {
event.preventDefault();
cancelEditing();
}
}}
onBlur={() => {
void commitName();
}}
/>
{error ? <p className="text-xs text-red-500">{error}</p> : null}
</div>
) : (
<div className="flex items-start gap-2">
<p className="min-w-0 flex-1 font-pixel text-base leading-snug text-foreground">
{displayName}
</p>
{editable ? (
<button
type="button"
aria-label="Edit pet name"
title="Edit pet name"
disabled={isSaving}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
startEditing();
}}
className="flex shrink-0 items-center justify-center border-2 border-border bg-card/80 p-1.5 text-muted-foreground shadow-[var(--retro-shadow-sm)] transition-colors hover:border-primary/60 hover:text-foreground"
>
<Pencil
aria-hidden="true"
className="size-3.5"
strokeWidth={2.5}
/>
</button>
) : null}
</div>
)}
<p className="mt-1.5 text-xs uppercase tracking-[0.2em] text-secondary">
{moodLabel}
</p>
</div>
);
}

View File

@@ -369,7 +369,7 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
/> />
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
In-app reminders appear inside Habit Pet while a tab is open. In-app reminders appear inside HaBit Pet while a tab is open.
Browser (OS) reminders use the Web Notifications API and may appear Browser (OS) reminders use the Web Notifications API and may appear
in your system notification center. Reminders fire once per day at in your system notification center. Reminders fire once per day at
your chosen local time. your chosen local time.

View File

@@ -174,7 +174,7 @@ export function showTestSystemDailyReminderNotification(
} }
const { body, targetUrl } = buildDailyReminderNotification(status); const { body, targetUrl } = buildDailyReminderNotification(status);
const notification = new Notification("Test: Habit Pet reminder", { const notification = new Notification("Test: HaBit Pet reminder", {
body: `This is a browser test notification. ${body}`, body: `This is a browser test notification. ${body}`,
tag: `habit-pet-daily-reminder-test-${Date.now()}`, tag: `habit-pet-daily-reminder-test-${Date.now()}`,
}); });

View File

@@ -46,7 +46,7 @@ export async function generateAiTaskLabels(
Authorization: `Bearer ${apiKey}`, Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json", "Content-Type": "application/json",
"HTTP-Referer": siteUrl, "HTTP-Referer": siteUrl,
"X-Title": "Habit Pet", "X-Title": "HaBit Pet",
}, },
body: JSON.stringify({ body: JSON.stringify({
model: DEFAULT_OPENROUTER_MODEL, model: DEFAULT_OPENROUTER_MODEL,

View File

@@ -3,7 +3,7 @@ export const reminderDeliveryOptions = [
value: "in_app", value: "in_app",
label: "In-app", label: "In-app",
description: description:
"Show reminders inside Habit Pet when you have the site open in a tab.", "Show reminders inside HaBit Pet when you have the site open in a tab.",
}, },
{ {
value: "system", value: "system",