Improve readability and randomize guest landing pet.

Pair Share Tech Mono body text with Press Start 2P display accents, and show a randomized dressed pixel character for signed-out homepage previews.
This commit is contained in:
2026-05-24 05:21:56 -06:00
parent 2b76e7f34c
commit b8596eacee
15 changed files with 153 additions and 21 deletions

View File

@@ -19,7 +19,7 @@
--secondary: 198 100% 45%;
--secondary-foreground: 252 38% 10%;
--muted: 252 24% 20%;
--muted-foreground: 52 40% 68%;
--muted-foreground: 52 35% 74%;
--accent: 142 70% 45%;
--accent-foreground: 252 38% 10%;
--destructive: 0 84% 58%;
@@ -51,7 +51,7 @@
--secondary: 198 100% 45%;
--secondary-foreground: 252 38% 10%;
--muted: 252 24% 20%;
--muted-foreground: 52 40% 68%;
--muted-foreground: 52 35% 74%;
--accent: 142 70% 45%;
--accent-foreground: 252 38% 10%;
--destructive: 0 84% 58%;
@@ -73,9 +73,16 @@
}
body {
@apply overflow-x-hidden bg-background text-foreground text-xs leading-relaxed;
font-family: var(--font-pixel), monospace;
image-rendering: pixelated;
@apply overflow-x-hidden bg-background text-foreground text-sm leading-relaxed;
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-pixel leading-snug;
}
}

View File

@@ -1,5 +1,5 @@
import type { Metadata, Viewport } from "next";
import { Press_Start_2P } from "next/font/google";
import { Press_Start_2P, Share_Tech_Mono } from "next/font/google";
import { ThemeProvider } from "next-themes";
import { GlobalBottomNav } from "@/components/layout/global-bottom-nav";
@@ -30,6 +30,13 @@ const pixelFont = Press_Start_2P({
display: "swap",
});
const bodyFont = Share_Tech_Mono({
weight: "400",
subsets: ["latin"],
variable: "--font-body",
display: "swap",
});
export default function RootLayout({
children,
}: Readonly<{
@@ -37,7 +44,7 @@ export default function RootLayout({
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${pixelFont.variable} font-sans`}>
<body className={`${pixelFont.variable} ${bodyFont.variable} font-sans antialiased`}>
<ThemeProvider
attribute="class"
defaultTheme="system"

View File

@@ -9,6 +9,7 @@ import {
getAvatarCustomization,
type AvatarCustomization,
} from "@/lib/avatar-customization-storage";
import { getGuestLandingPetCustomization } from "@/lib/landing-random-pet";
import { routes } from "@/lib/routes";
import { createClient } from "@/lib/supabase/client";
import { cn } from "@/lib/utils";
@@ -76,7 +77,7 @@ export function LandingPetPreview() {
if (!user) {
setIsSignedIn(false);
setCustomization(defaultAvatarCustomization);
setCustomization(getGuestLandingPetCustomization());
setIsLoading(false);
return;
}

View File

@@ -93,7 +93,7 @@ export const AppNav = forwardRef<HTMLElement, AppNavProps>(function AppNav(
onPointerEnter={() => prefetchByRoute[item.href]?.()}
onFocus={() => prefetchByRoute[item.href]?.()}
className={cn(
"bottom-nav-item flex flex-col items-center justify-center gap-1.5 overflow-hidden border-2 px-1 py-2 text-[9px] leading-none transition-colors duration-75",
"bottom-nav-item flex flex-col items-center justify-center gap-1.5 overflow-hidden border-2 px-1 py-2 font-pixel text-[8px] leading-none transition-colors duration-75",
isActive
? "border-secondary bg-primary text-primary-foreground shadow-[var(--retro-shadow-sm)]"
: "border-transparent text-muted-foreground hover:border-border hover:bg-muted hover:text-foreground",

View File

@@ -16,9 +16,9 @@ export function AppPageHeader({
{eyebrow}
</p>
) : null}
<h1 className="text-sm">{title}</h1>
<h1 className="text-base">{title}</h1>
{description ? (
<p className="mt-2 text-xs leading-relaxed text-muted-foreground">
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
{description}
</p>
) : null}

View File

@@ -28,7 +28,7 @@ export function AuthBottomNav() {
key={item.href}
href={item.href}
className={cn(
"flex flex-col items-center gap-1 rounded-lg px-1 py-2 text-[9px] font-medium leading-tight transition-colors",
"flex flex-col items-center gap-1 rounded-lg px-1 py-2 font-pixel text-[8px] font-normal leading-tight transition-colors",
isActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground",

View File

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

View File

@@ -11,7 +11,7 @@ export function SiteHeader() {
return (
<header className="w-full border-b border-b-foreground/10">
<div className="mx-auto flex h-16 w-full max-w-5xl items-center justify-between px-5 text-sm">
<Link href={routes.home} className="font-semibold tracking-tight">
<Link href={routes.home} className="font-pixel text-xs tracking-tight">
Habit Pet
</Link>
{!hasEnvVars ? (

View File

@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-none border-2 border-border px-2 py-0.5 text-[9px] font-semibold uppercase tracking-wider transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 shadow-[var(--retro-shadow-sm)]",
"inline-flex items-center rounded-none border-2 border-border px-2 py-0.5 text-[10px] font-normal uppercase tracking-wide transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 shadow-[var(--retro-shadow-sm)]",
{
variants: {
variant: {

View File

@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap border-2 border-border text-xs font-normal transition-[transform,box-shadow] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center gap-2 whitespace-nowrap border-2 border-border font-pixel text-[10px] font-normal uppercase tracking-wide transition-[transform,box-shadow] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {

View File

@@ -35,7 +35,7 @@ const CardTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
className={cn("font-pixel text-sm font-normal leading-snug tracking-tight", className)}
{...props}
/>
));
@@ -47,7 +47,7 @@ const CardDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn("text-sm leading-relaxed text-muted-foreground", className)}
{...props}
/>
));

View File

@@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
<input
type={type}
className={cn(
"flex h-9 w-full rounded-none border-2 border-input bg-muted/30 px-3 py-1 text-xs shadow-[var(--retro-shadow-sm)] transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
"flex h-9 w-full rounded-none border-2 border-input bg-muted/30 px-3 py-1 text-sm leading-normal shadow-[var(--retro-shadow-sm)] transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}

View File

@@ -7,7 +7,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
"text-sm font-normal leading-snug peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<

117
lib/landing-random-pet.ts Normal file
View File

@@ -0,0 +1,117 @@
import {
DEFAULT_AVATAR_NAME,
type AvatarCustomization,
} from "@/lib/avatar-customization-storage";
import {
CHARACTER_LAYERS,
COLOR_PRESETS,
NONE_VARIANT_ID,
type CharacterLayerId,
} from "@/lib/character/presets";
import { ROOM_BACKGROUNDS } from "@/lib/room-backgrounds";
const GUEST_PET_STORAGE_KEY = "habit-pet-guest-landing-pet";
const SKIN_TONE_IDS = [
"porcelain",
"fair",
"light",
"medium",
"tan",
"brown",
"dark",
"deep",
"onyx",
] as const;
const OUTFIT_COLOR_IDS = [
"red",
"orange",
"yellow",
"green",
"blue",
"indigo",
"black",
"white",
] as const;
const EYE_COLOR_IDS = ["blue", "black", "green", "brown"] as const;
const HAIR_COLOR_IDS = ["brown", "black", "yellow", "red", "orange", "white"] as const;
function pickRandom<T>(items: readonly T[]): T {
return items[Math.floor(Math.random() * items.length)]!;
}
function presetHsl(id: string) {
const preset = COLOR_PRESETS.find((entry) => entry.id === id);
if (!preset) {
throw new Error(`Missing color preset: ${id}`);
}
return { ...preset.hsl };
}
function pickRandomVariant(
layerId: Exclude<CharacterLayerId, "skin">,
allowNone = false,
): string {
const layer = CHARACTER_LAYERS.find((entry) => entry.id === layerId);
if (!layer) {
throw new Error(`Missing character layer: ${layerId}`);
}
const options = layer.variants
.map((variant) => variant.id)
.filter((id) => allowNone || id !== NONE_VARIANT_ID);
return pickRandom(options);
}
export function buildRandomLandingPetCustomization(): AvatarCustomization {
const freeRooms = ROOM_BACKGROUNDS.filter((room) => room.free).map(
(room) => room.id,
);
return {
name: DEFAULT_AVATAR_NAME,
colors: {
skin: presetHsl(pickRandom(SKIN_TONE_IDS)),
pants: presetHsl(pickRandom(OUTFIT_COLOR_IDS)),
shoes: presetHsl(pickRandom(OUTFIT_COLOR_IDS)),
torso: presetHsl(pickRandom(OUTFIT_COLOR_IDS)),
eyes: presetHsl(pickRandom(EYE_COLOR_IDS)),
head: presetHsl(pickRandom(HAIR_COLOR_IDS)),
},
variants: {
skin: "skin-1",
pants: pickRandomVariant("pants"),
shoes: pickRandomVariant("shoes"),
torso: pickRandomVariant("torso"),
eyes: pickRandomVariant("eyes"),
head: pickRandomVariant("head", Math.random() < 0.15),
},
customized: true,
equippedItems: [],
roomBackground: pickRandom(freeRooms),
};
}
export function getGuestLandingPetCustomization(): AvatarCustomization {
if (typeof window === "undefined") {
return buildRandomLandingPetCustomization();
}
try {
const cached = sessionStorage.getItem(GUEST_PET_STORAGE_KEY);
if (cached) {
return JSON.parse(cached) as AvatarCustomization;
}
const next = buildRandomLandingPetCustomization();
sessionStorage.setItem(GUEST_PET_STORAGE_KEY, JSON.stringify(next));
return next;
} catch {
return buildRandomLandingPetCustomization();
}
}

View File

@@ -60,7 +60,7 @@ export default {
sm: "calc(var(--radius) - 4px)",
},
fontFamily: {
sans: ["var(--font-pixel)", "monospace"],
sans: ["var(--font-body)", "monospace"],
pixel: ["var(--font-pixel)", "monospace"],
},
spacing: {