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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
117
lib/landing-random-pet.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user