Fix room layout proportions, responsive sizing, and visual composition
Addresses layout issues where the room felt cut off and controls were disproportionate, especially on mobile. Key changes: RoomActionButton — responsive sizing: Mobile: size-14 circle, size-7 icons Desktop: size-20 circle, size-9 icons Tighter gap-1 and text-[10px] labels on mobile ItemCarousel — responsive behavior: Mobile: focused item only + compact arrows, no prev/next previews Desktop: focused item + translucent side previews Smaller focused item (text-4xl mobile, text-5xl desktop) BlobbiRoomHero — reduced visual footprint: Blobbi visual: tighter responsive scale (size-48 -> size-60 -> size-80) Stats crown: reduced margin (mb-6 sm:mb-10 vs mb-14) Stat indicators: size-14 sm:size-[4.5rem] (was size-[4.5rem] sm:size-20) Glow blur reduced on mobile (-m-16 vs -m-24) overflow-hidden to prevent hero from clipping HomeRoom — unified bottom bar: Removed -mt-10 negative margin hack that caused visual clipping Photo, Carousel, and Companion now sit in one flex row with items-center alignment, forming a single cohesive bottom composition All rooms — standardized bottom bar pattern: Consistent px-3 sm:px-6 pb-4 sm:pb-6 pt-1 spacing flex items-center justify-between gap-1 sm:gap-3 layout Spacer divs match button widths (w-14 sm:w-20) for balance Room shell content area uses flex flex-col to ensure rooms fill the available vertical space correctly.
This commit is contained in:
@@ -3,13 +3,7 @@
|
||||
/**
|
||||
* BlobbiCareRoom — The bathroom / hygiene room.
|
||||
*
|
||||
* Layout:
|
||||
* - BlobbiRoomHero (Blobbi visual + stats)
|
||||
* - Center: single-focus carousel with hygiene + medicine items
|
||||
* - Bottom left: towel (RoomActionButton style)
|
||||
* - Bottom right: shower (RoomActionButton style)
|
||||
*
|
||||
* Future: interactions could become drag-based.
|
||||
* Bottom bar: Towel (left) | hygiene+medicine carousel (center) | Shower (right)
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
@@ -40,48 +34,41 @@ export function BlobbiCareRoom({ ctx }: BlobbiCareRoomProps) {
|
||||
[]);
|
||||
|
||||
const isDisabled = isPublishing || actionInProgress !== null || isUsingItem;
|
||||
|
||||
// Towel is shown as a dedicated button, not in the carousel
|
||||
const towelItem = hygieneItems.find(i => i.id === 'hyg_towel');
|
||||
|
||||
// Carousel: hygiene (except towel) + medicine
|
||||
const carouselEntries = useMemo<CarouselEntry[]>(() => {
|
||||
const hygiene = getLiveShopItems()
|
||||
.filter(i => i.type === 'hygiene' && i.id !== 'hyg_towel')
|
||||
.map(i => ({ id: i.id, icon: <span>{i.icon}</span>, label: i.name }));
|
||||
|
||||
const medicine = getLiveShopItems()
|
||||
.filter(i => i.type === 'medicine')
|
||||
.map(i => ({ id: i.id, icon: <span>{i.icon}</span>, label: i.name }));
|
||||
|
||||
return [...hygiene, ...medicine];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
{/* ── Hero ── */}
|
||||
<BlobbiRoomHero ctx={ctx} className="flex-1 min-h-0" />
|
||||
|
||||
{/* ── Bottom Action Bar ── */}
|
||||
{!isActiveFloatingCompanion && (
|
||||
<div className="relative z-10 px-4 sm:px-8 pb-6 pt-2">
|
||||
<div className="flex items-start justify-between">
|
||||
{/* Bottom left — Towel */}
|
||||
<div className="w-24 shrink-0">
|
||||
{towelItem && (
|
||||
<RoomActionButton
|
||||
icon={<span className="text-3xl sm:text-4xl">{towelItem.icon}</span>}
|
||||
label="Towel"
|
||||
color="text-cyan-500"
|
||||
glowHex="#06b6d4"
|
||||
onClick={() => handleUseItemFromTab(towelItem.id)}
|
||||
disabled={isDisabled}
|
||||
loading={isUsingItem && usingItemId === towelItem.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative z-10 px-3 sm:px-6 pb-4 sm:pb-6 pt-1">
|
||||
<div className="flex items-center justify-between gap-1 sm:gap-3">
|
||||
{/* Left — Towel */}
|
||||
{towelItem ? (
|
||||
<RoomActionButton
|
||||
icon={<span className="text-2xl sm:text-3xl">{towelItem.icon}</span>}
|
||||
label="Towel"
|
||||
color="text-cyan-500"
|
||||
glowHex="#06b6d4"
|
||||
onClick={() => handleUseItemFromTab(towelItem.id)}
|
||||
disabled={isDisabled}
|
||||
loading={isUsingItem && usingItemId === towelItem.id}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-14 sm:w-20 shrink-0" />
|
||||
)}
|
||||
|
||||
{/* Center: single-focus carousel */}
|
||||
{/* Center carousel */}
|
||||
<div className="flex-1 min-w-0 flex justify-center">
|
||||
<ItemCarousel
|
||||
items={carouselEntries}
|
||||
@@ -91,9 +78,9 @@ export function BlobbiCareRoom({ ctx }: BlobbiCareRoomProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bottom right — Shower */}
|
||||
{/* Right — Shower */}
|
||||
<RoomActionButton
|
||||
icon={<ShowerHead className="size-9 sm:size-10" />}
|
||||
icon={<ShowerHead className="size-7 sm:size-9" />}
|
||||
label="Shower"
|
||||
color="text-blue-500"
|
||||
glowHex="#3b82f6"
|
||||
|
||||
@@ -119,24 +119,24 @@ export function BlobbiHatcheryRoom({ ctx }: BlobbiHatcheryRoomProps) {
|
||||
|
||||
{/* ── Bottom Action Bar ── */}
|
||||
{!isActiveFloatingCompanion && (
|
||||
<div className="relative z-10 px-4 sm:px-8 pb-6 pt-2">
|
||||
<div className="flex items-start justify-between">
|
||||
{/* Bottom left — Blobbis selector */}
|
||||
<div className="relative z-10 px-3 sm:px-6 pb-4 sm:pb-6 pt-1">
|
||||
<div className="flex items-center justify-between gap-1 sm:gap-3">
|
||||
{/* Left — Blobbis selector */}
|
||||
<RoomActionButton
|
||||
icon={<Egg className="size-9 sm:size-10" />}
|
||||
icon={<Egg className="size-7 sm:size-9" />}
|
||||
label="Blobbis"
|
||||
color="text-primary"
|
||||
glowHex="var(--primary)"
|
||||
onClick={() => setShowBlobbisPanel(true)}
|
||||
badge={companions.length > 1 ? (
|
||||
<span className="size-5 rounded-full bg-primary text-[10px] text-primary-foreground font-bold flex items-center justify-center">
|
||||
<span className="size-4 sm:size-5 rounded-full bg-primary text-[9px] sm:text-[10px] text-primary-foreground font-bold flex items-center justify-center">
|
||||
{companions.length}
|
||||
</span>
|
||||
) : undefined}
|
||||
/>
|
||||
|
||||
{/* Center — Main hatch/evolve action */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-1.5 pt-4">
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-1.5">
|
||||
{/* Active process: Hatch/Evolve CTA or progress */}
|
||||
{hasActiveProcess && allCompleted && !isTasksLoading && (
|
||||
<button
|
||||
@@ -229,15 +229,15 @@ export function BlobbiHatcheryRoom({ ctx }: BlobbiHatcheryRoomProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom right — Quests/Tasks */}
|
||||
{/* Right — Quests/Tasks */}
|
||||
<RoomActionButton
|
||||
icon={<ListTodo className="size-9 sm:size-10" />}
|
||||
icon={<ListTodo className="size-7 sm:size-9" />}
|
||||
label="Quests"
|
||||
color="text-amber-500"
|
||||
glowHex="#f59e0b"
|
||||
onClick={() => setShowQuestsPanel(true)}
|
||||
badge={hasActiveProcess && totalCount - completedCount > 0 ? (
|
||||
<span className="size-5 rounded-full bg-amber-500 text-[10px] text-white font-bold flex items-center justify-center">
|
||||
<span className="size-4 sm:size-5 rounded-full bg-amber-500 text-[9px] sm:text-[10px] text-white font-bold flex items-center justify-center">
|
||||
{totalCount - completedCount}
|
||||
</span>
|
||||
) : undefined}
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
*
|
||||
* Layout:
|
||||
* - BlobbiRoomHero (stats crown, Blobbi visual, name)
|
||||
* - Photo button (left) + Companion toggle (right) — same style as original
|
||||
* - Center: single-focus carousel with toys + music + sing
|
||||
* - Inline activity (music player, sing card) between hero and bottom bar
|
||||
* - Unified bottom bar: Photo (left) | Carousel (center) | Companion (right)
|
||||
* - Inline activity (music player, sing card) above the bottom bar
|
||||
*
|
||||
* Sleep/wake has been moved to BlobbiRestRoom.
|
||||
*/
|
||||
@@ -29,20 +28,16 @@ interface BlobbiHomeRoomProps {
|
||||
export function BlobbiHomeRoom({ ctx }: BlobbiHomeRoomProps) {
|
||||
const {
|
||||
isActiveFloatingCompanion,
|
||||
// Photo
|
||||
setShowPhotoModal,
|
||||
// Companion
|
||||
isCurrentCompanion,
|
||||
canBeCompanion,
|
||||
isUpdatingCompanion,
|
||||
handleSetAsCompanion,
|
||||
// Items + actions
|
||||
isUsingItem,
|
||||
usingItemId,
|
||||
handleUseItemFromTab,
|
||||
handleDirectAction,
|
||||
isDirectActionPending,
|
||||
// Inline activity
|
||||
inlineActivity,
|
||||
handleConfirmSing,
|
||||
handleCloseInlineActivity,
|
||||
@@ -51,7 +46,6 @@ export function BlobbiHomeRoom({ ctx }: BlobbiHomeRoomProps) {
|
||||
handleSingRecordingStart,
|
||||
handleSingRecordingStop,
|
||||
handleChangeTrack,
|
||||
// State
|
||||
isPublishing,
|
||||
actionInProgress,
|
||||
} = ctx;
|
||||
@@ -65,12 +59,12 @@ export function BlobbiHomeRoom({ ctx }: BlobbiHomeRoomProps) {
|
||||
const actions: CarouselEntry[] = [
|
||||
{
|
||||
id: '__action_music',
|
||||
icon: <div className="size-12 rounded-full flex items-center justify-center bg-pink-500/15 text-pink-500"><Music className="size-6" /></div>,
|
||||
icon: <div className="size-10 sm:size-12 rounded-full flex items-center justify-center bg-pink-500/15 text-pink-500"><Music className="size-5 sm:size-6" /></div>,
|
||||
label: 'Music',
|
||||
},
|
||||
{
|
||||
id: '__action_sing',
|
||||
icon: <div className="size-12 rounded-full flex items-center justify-center bg-purple-500/15 text-purple-500"><Mic className="size-6" /></div>,
|
||||
icon: <div className="size-10 sm:size-12 rounded-full flex items-center justify-center bg-purple-500/15 text-purple-500"><Mic className="size-5 sm:size-6" /></div>,
|
||||
label: 'Sing',
|
||||
},
|
||||
];
|
||||
@@ -95,33 +89,6 @@ export function BlobbiHomeRoom({ ctx }: BlobbiHomeRoomProps) {
|
||||
{/* ── Hero (Blobbi + stats) ── */}
|
||||
<BlobbiRoomHero ctx={ctx} className="flex-1 min-h-0" />
|
||||
|
||||
{/* ── Action circles — Photo (left) + Companion (right) ── */}
|
||||
{!isActiveFloatingCompanion && (
|
||||
<div className="relative z-10 w-full px-4 sm:px-8 -mt-10 flex items-start justify-between">
|
||||
{/* Photo */}
|
||||
<RoomActionButton
|
||||
icon={<Camera className="size-9 sm:size-10" />}
|
||||
label="Photo"
|
||||
color="text-pink-500"
|
||||
glowHex="#ec4899"
|
||||
onClick={() => setShowPhotoModal(true)}
|
||||
/>
|
||||
|
||||
{/* Companion toggle */}
|
||||
{canBeCompanion && (
|
||||
<RoomActionButton
|
||||
icon={<Footprints className="size-9 sm:size-10" />}
|
||||
label={isCurrentCompanion ? 'With you' : 'Take along'}
|
||||
color={isCurrentCompanion ? 'text-emerald-500' : 'text-violet-500'}
|
||||
glowHex={isCurrentCompanion ? '#10b981' : '#8b5cf6'}
|
||||
onClick={handleSetAsCompanion}
|
||||
disabled={isUpdatingCompanion}
|
||||
loading={isUpdatingCompanion}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Inline Activity Area (music/sing) ── */}
|
||||
{inlineActivity.type === 'music' && (
|
||||
<div className="px-4 sm:px-6 pb-2">
|
||||
@@ -148,15 +115,44 @@ export function BlobbiHomeRoom({ ctx }: BlobbiHomeRoomProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Bottom: Single-focus carousel ── */}
|
||||
{/* ── Unified Bottom Bar: Photo | Carousel | Companion ── */}
|
||||
{!isActiveFloatingCompanion && (
|
||||
<div className="relative z-10 px-2 pb-4 pt-2">
|
||||
<ItemCarousel
|
||||
items={carouselItems}
|
||||
onUse={handleCarouselUse}
|
||||
activeItemId={isUsingItem ? usingItemId : null}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<div className="relative z-10 px-3 sm:px-6 pb-4 sm:pb-6 pt-1">
|
||||
<div className="flex items-center justify-between gap-1 sm:gap-3">
|
||||
{/* Photo */}
|
||||
<RoomActionButton
|
||||
icon={<Camera className="size-7 sm:size-9" />}
|
||||
label="Photo"
|
||||
color="text-pink-500"
|
||||
glowHex="#ec4899"
|
||||
onClick={() => setShowPhotoModal(true)}
|
||||
/>
|
||||
|
||||
{/* Center carousel */}
|
||||
<div className="flex-1 min-w-0 flex justify-center">
|
||||
<ItemCarousel
|
||||
items={carouselItems}
|
||||
onUse={handleCarouselUse}
|
||||
activeItemId={isUsingItem ? usingItemId : null}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Companion toggle */}
|
||||
{canBeCompanion ? (
|
||||
<RoomActionButton
|
||||
icon={<Footprints className="size-7 sm:size-9" />}
|
||||
label={isCurrentCompanion ? 'With you' : 'Take along'}
|
||||
color={isCurrentCompanion ? 'text-emerald-500' : 'text-violet-500'}
|
||||
glowHex={isCurrentCompanion ? '#10b981' : '#8b5cf6'}
|
||||
onClick={handleSetAsCompanion}
|
||||
disabled={isUpdatingCompanion}
|
||||
loading={isUpdatingCompanion}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-14 sm:w-20 shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,7 @@
|
||||
/**
|
||||
* BlobbiKitchenRoom — The feeding room.
|
||||
*
|
||||
* Layout:
|
||||
* - BlobbiRoomHero (Blobbi visual + stats)
|
||||
* - Center: single-focus food carousel
|
||||
* - Bottom right: fridge button (opens full food list modal)
|
||||
* - Bottom left: empty for now
|
||||
* Bottom bar: (empty left) | food carousel (center) | Fridge button (right)
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
@@ -38,7 +34,6 @@ export function BlobbiKitchenRoom({ ctx }: BlobbiKitchenRoomProps) {
|
||||
|
||||
const [showFridge, setShowFridge] = useState(false);
|
||||
|
||||
// Food carousel entries
|
||||
const foodEntries = useMemo<CarouselEntry[]>(() =>
|
||||
getLiveShopItems()
|
||||
.filter(i => i.type === 'food')
|
||||
@@ -47,7 +42,6 @@ export function BlobbiKitchenRoom({ ctx }: BlobbiKitchenRoomProps) {
|
||||
|
||||
const isDisabled = isPublishing || actionInProgress !== null || isUsingItem;
|
||||
|
||||
// Handler for using an item from the fridge modal
|
||||
const handleFridgeUseItem = (itemId: string) => {
|
||||
if (isUsingItem) return;
|
||||
ctx.onUseItem(itemId, 'feed').finally(() => {
|
||||
@@ -57,17 +51,15 @@ export function BlobbiKitchenRoom({ ctx }: BlobbiKitchenRoomProps) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
{/* ── Hero ── */}
|
||||
<BlobbiRoomHero ctx={ctx} className="flex-1 min-h-0" />
|
||||
|
||||
{/* ── Bottom Action Bar ── */}
|
||||
{!isActiveFloatingCompanion && (
|
||||
<div className="relative z-10 px-4 sm:px-8 pb-6 pt-2">
|
||||
<div className="flex items-start justify-between">
|
||||
{/* Bottom left — empty */}
|
||||
<div className="w-24 shrink-0" />
|
||||
<div className="relative z-10 px-3 sm:px-6 pb-4 sm:pb-6 pt-1">
|
||||
<div className="flex items-center justify-between gap-1 sm:gap-3">
|
||||
{/* Left — empty spacer matching button width */}
|
||||
<div className="w-14 sm:w-20 shrink-0" />
|
||||
|
||||
{/* Center: single-focus food carousel */}
|
||||
{/* Center: food carousel */}
|
||||
<div className="flex-1 min-w-0 flex justify-center">
|
||||
<ItemCarousel
|
||||
items={foodEntries}
|
||||
@@ -77,9 +69,9 @@ export function BlobbiKitchenRoom({ ctx }: BlobbiKitchenRoomProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bottom right — Fridge */}
|
||||
{/* Right — Fridge */}
|
||||
<RoomActionButton
|
||||
icon={<Refrigerator className="size-9 sm:size-10" />}
|
||||
icon={<Refrigerator className="size-7 sm:size-9" />}
|
||||
label="Fridge"
|
||||
color="text-orange-500"
|
||||
glowHex="#f97316"
|
||||
@@ -90,7 +82,6 @@ export function BlobbiKitchenRoom({ ctx }: BlobbiKitchenRoomProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Fridge Modal ── */}
|
||||
{showFridge && (
|
||||
<BlobbiActionInventoryModal
|
||||
open={showFridge}
|
||||
|
||||
@@ -3,12 +3,7 @@
|
||||
/**
|
||||
* BlobbiRestRoom — The bedroom / rest room.
|
||||
*
|
||||
* Dedicated to sleep / wake / resting.
|
||||
* Layout:
|
||||
* - BlobbiRoomHero (Blobbi visual + stats)
|
||||
* - Bottom right: sleep / wake toggle (unified RoomActionButton style)
|
||||
* - Bottom center: minimal for now
|
||||
* - Bottom left: empty for now
|
||||
* Bottom bar: (empty left) | (empty center) | Sleep/Wake (right)
|
||||
*/
|
||||
|
||||
import { Moon, Sun, Loader2 } from 'lucide-react';
|
||||
@@ -36,28 +31,26 @@ export function BlobbiRestRoom({ ctx }: BlobbiRestRoomProps) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
{/* ── Hero ── */}
|
||||
<BlobbiRoomHero ctx={ctx} className="flex-1 min-h-0" />
|
||||
|
||||
{/* ── Bottom Action Bar ── */}
|
||||
{!isActiveFloatingCompanion && (
|
||||
<div className="relative z-10 px-4 sm:px-8 pb-6 pt-2">
|
||||
<div className="flex items-start justify-between">
|
||||
{/* Bottom left — empty for now */}
|
||||
<div className="w-24 shrink-0" />
|
||||
<div className="relative z-10 px-3 sm:px-6 pb-4 sm:pb-6 pt-1">
|
||||
<div className="flex items-center justify-between gap-1 sm:gap-3">
|
||||
{/* Left — empty */}
|
||||
<div className="w-14 sm:w-20 shrink-0" />
|
||||
|
||||
{/* Center — minimal for now */}
|
||||
{/* Center — empty */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Bottom right — Sleep / Wake */}
|
||||
{!isEgg && (
|
||||
{/* Right — Sleep / Wake */}
|
||||
{!isEgg ? (
|
||||
<RoomActionButton
|
||||
icon={
|
||||
actionInProgress === 'rest'
|
||||
? <Loader2 className="size-9 sm:size-10 animate-spin" />
|
||||
? <Loader2 className="size-7 sm:size-9 animate-spin" />
|
||||
: isSleeping
|
||||
? <Sun className="size-9 sm:size-10" />
|
||||
: <Moon className="size-9 sm:size-10" />
|
||||
? <Sun className="size-7 sm:size-9" />
|
||||
: <Moon className="size-7 sm:size-9" />
|
||||
}
|
||||
label={isSleeping ? 'Wake up' : 'Sleep'}
|
||||
color={isSleeping ? 'text-amber-500' : 'text-violet-500'}
|
||||
@@ -65,6 +58,8 @@ export function BlobbiRestRoom({ ctx }: BlobbiRestRoomProps) {
|
||||
onClick={onRest}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-14 sm:w-20 shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
* - Blobbi name (hidden for eggs)
|
||||
* - Bob/sway animation when not sleeping
|
||||
*
|
||||
* This component extracts the hero section that was previously inlined
|
||||
* in BlobbiDashboard so all rooms can share it without duplication.
|
||||
* Sizing is tuned so the hero doesn't crowd the bottom action bar:
|
||||
* - Stats indicators are smaller on mobile (size-14 vs size-20)
|
||||
* - Blobbi visual uses a tighter responsive scale
|
||||
* - Stats crown margin is reduced
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
@@ -24,7 +26,7 @@ import { getVisibleStats, getStatStatus } from '@/blobbi/core/lib/blobbi-decay';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { BlobbiRoomContext } from '../lib/room-types';
|
||||
|
||||
// ─── Stat colour maps (same as original) ──────────────────────────────────────
|
||||
// ─── Stat colour maps ─────────────────────────────────────────────────────────
|
||||
|
||||
const STAT_COLOR_MAP: Record<string, 'orange' | 'yellow' | 'green' | 'blue' | 'violet'> = {
|
||||
hunger: 'orange',
|
||||
@@ -70,11 +72,8 @@ const STAT_ICON_MAP: Record<string, React.ComponentType<{ className?: string; st
|
||||
|
||||
interface BlobbiRoomHeroProps {
|
||||
ctx: BlobbiRoomContext;
|
||||
/** Optional extra className on the outer container */
|
||||
className?: string;
|
||||
/** If true, hides the stats crown */
|
||||
hideStats?: boolean;
|
||||
/** If true, hides the name */
|
||||
hideName?: boolean;
|
||||
}
|
||||
|
||||
@@ -101,8 +100,8 @@ export function BlobbiRoomHero({ ctx, className, hideStats, hideName }: BlobbiRo
|
||||
// When the companion is out floating, show "out exploring" instead
|
||||
if (isActiveFloatingCompanion) {
|
||||
return (
|
||||
<div className={cn('flex flex-col items-center justify-center gap-6 text-center flex-1', className)}>
|
||||
<Footprints className="size-16 text-muted-foreground/30" />
|
||||
<div className={cn('flex flex-col items-center justify-center gap-4 text-center flex-1 px-4', className)}>
|
||||
<Footprints className="size-12 text-muted-foreground/30" />
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{companion.name} is out exploring right now.
|
||||
</p>
|
||||
@@ -110,16 +109,16 @@ export function BlobbiRoomHero({ ctx, className, hideStats, hideName }: BlobbiRo
|
||||
onClick={handleSetAsCompanion}
|
||||
disabled={isUpdatingCompanion}
|
||||
className={cn(
|
||||
'flex items-center justify-center gap-2.5 px-8 py-3.5 rounded-full text-white font-semibold transition-all duration-300 ease-out',
|
||||
'flex items-center justify-center gap-2 px-6 py-3 rounded-full text-white font-semibold transition-all duration-300 ease-out text-sm',
|
||||
'hover:-translate-y-0.5 hover:scale-105 hover:brightness-110 active:scale-95',
|
||||
isUpdatingCompanion && 'opacity-50 pointer-events-none',
|
||||
)}
|
||||
style={{ background: 'linear-gradient(135deg, #8b5cf6, #ec4899, #f59e0b)' }}
|
||||
>
|
||||
{isUpdatingCompanion ? (
|
||||
<Loader2 className="size-5 animate-spin" />
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
<Footprints className="size-5" />
|
||||
<Footprints className="size-4" />
|
||||
)}
|
||||
<span>Bring {companion.name} home</span>
|
||||
</button>
|
||||
@@ -128,7 +127,7 @@ export function BlobbiRoomHero({ ctx, className, hideStats, hideName }: BlobbiRo
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={heroRef} className={cn('relative flex flex-col items-center justify-center px-4 sm:px-6 overflow-x-hidden flex-1', className)}>
|
||||
<div ref={heroRef} className={cn('relative flex flex-col items-center justify-center px-4 sm:px-6 overflow-hidden flex-1 min-h-0', className)}>
|
||||
<div className="relative flex flex-col items-center">
|
||||
{/* Stats crown */}
|
||||
{!hideStats && <StatsCrown companion={companion} currentStats={currentStats} heroWidth={heroWidth} />}
|
||||
@@ -140,7 +139,7 @@ export function BlobbiRoomHero({ ctx, className, hideStats, hideName }: BlobbiRo
|
||||
animation: `blobbi-bob ${4 - (currentStats.happiness / 100) * 1.5}s ease-in-out infinite, blobbi-sway ${6 - (currentStats.happiness / 100) * 2}s ease-in-out infinite`,
|
||||
} : undefined}
|
||||
>
|
||||
<div className="absolute inset-0 -m-24 bg-primary/5 rounded-full blur-3xl pointer-events-none" />
|
||||
<div className="absolute inset-0 -m-16 sm:-m-24 bg-primary/5 rounded-full blur-3xl pointer-events-none" />
|
||||
<BlobbiStageVisual
|
||||
companion={companion}
|
||||
size="lg"
|
||||
@@ -150,8 +149,8 @@ export function BlobbiRoomHero({ ctx, className, hideStats, hideName }: BlobbiRo
|
||||
recipeLabel={hasDevOverride ? undefined : statusRecipeLabel}
|
||||
emotion={effectiveEmotion}
|
||||
className={isEgg
|
||||
? 'size-44 min-[400px]:size-52 sm:size-64 md:size-80 lg:size-96'
|
||||
: 'size-64 min-[400px]:size-80 sm:size-96 md:size-[32rem] lg:size-[36rem]'
|
||||
? 'size-36 min-[400px]:size-44 sm:size-56 md:size-72 lg:size-80'
|
||||
: 'size-48 min-[400px]:size-60 sm:size-80 md:size-[28rem] lg:size-[32rem]'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -159,7 +158,7 @@ export function BlobbiRoomHero({ ctx, className, hideStats, hideName }: BlobbiRo
|
||||
{/* Blobbi Name — hidden for eggs */}
|
||||
{!hideName && !isEgg && (
|
||||
<h2
|
||||
className="text-2xl sm:text-3xl font-bold text-center -mt-2"
|
||||
className="text-xl sm:text-2xl md:text-3xl font-bold text-center mt-1"
|
||||
style={{ color: companion.visualTraits.baseColor }}
|
||||
>
|
||||
{companion.name}
|
||||
@@ -195,7 +194,7 @@ function StatsCrown({
|
||||
const count = allStats.length;
|
||||
const isSmall = heroWidth < 400;
|
||||
const arcSpread = isSmall
|
||||
? (count <= 2 ? 90 : count <= 3 ? 130 : 160)
|
||||
? (count <= 2 ? 80 : count <= 3 ? 120 : 150)
|
||||
: (count <= 2 ? 80 : count <= 3 ? 120 : 160);
|
||||
const arcHalf = arcSpread / 2;
|
||||
const angles = count === 1
|
||||
@@ -203,11 +202,11 @@ function StatsCrown({
|
||||
: allStats.map((_, i) => -arcHalf + (arcSpread / (count - 1)) * i);
|
||||
|
||||
return (
|
||||
<div className="relative flex items-end justify-center w-full mb-14" style={{ height: 48 }}>
|
||||
<div className="relative flex items-end justify-center w-full mb-6 sm:mb-10" style={{ height: 40 }}>
|
||||
{allStats.map((s, i) => {
|
||||
const angleDeg = angles[i];
|
||||
const angleRad = (angleDeg * Math.PI) / 180;
|
||||
const radius = Math.min(210, Math.max(140, (heroWidth - 340) / (640 - 340) * (210 - 140) + 140));
|
||||
const radius = Math.min(180, Math.max(100, (heroWidth - 340) / (640 - 340) * (180 - 100) + 100));
|
||||
const x = Math.sin(angleRad) * radius;
|
||||
const y = Math.cos(angleRad) * radius - radius;
|
||||
|
||||
@@ -251,7 +250,7 @@ function StatIndicator({ stat, value, color, status = 'normal' }: StatIndicatorP
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative size-[4.5rem] sm:size-20 rounded-full flex items-center justify-center',
|
||||
'relative size-14 sm:size-[4.5rem] rounded-full flex items-center justify-center',
|
||||
STAT_BG_COLORS[color],
|
||||
status === 'critical' && 'animate-pulse',
|
||||
)}>
|
||||
@@ -265,10 +264,10 @@ function StatIndicator({ stat, value, color, status = 'normal' }: StatIndicatorP
|
||||
/>
|
||||
</svg>
|
||||
<div className="relative">
|
||||
{IconComponent && <IconComponent className={cn('size-6 sm:size-7', STAT_COLORS[color])} strokeWidth={2.5} />}
|
||||
{IconComponent && <IconComponent className={cn('size-5 sm:size-6', STAT_COLORS[color])} strokeWidth={2.5} />}
|
||||
{isLow && (
|
||||
<AlertTriangle
|
||||
className={cn('absolute -top-1.5 -right-2 size-3.5', status === 'critical' ? 'text-red-500' : 'text-amber-500')}
|
||||
className={cn('absolute -top-1.5 -right-2 size-3', status === 'critical' ? 'text-red-500' : 'text-amber-500')}
|
||||
strokeWidth={3}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -147,7 +147,7 @@ export function BlobbiRoomShell({
|
||||
The `direction` in nav state tells which way to animate.
|
||||
For now, we simply render the active room instantly.
|
||||
*/}
|
||||
<div className="flex-1 min-h-0 relative">
|
||||
<div className="flex-1 min-h-0 flex flex-col relative">
|
||||
<RoomComponent ctx={ctx} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
/**
|
||||
* ItemCarousel — Single-focus carousel for room items.
|
||||
*
|
||||
* Shows one main item at centre with translucent prev/next previews.
|
||||
* Left/right arrows cycle through items in a loop.
|
||||
* Mobile: focused item only + compact arrows (no prev/next previews)
|
||||
* Desktop: focused item + translucent prev/next previews + arrows
|
||||
*
|
||||
* Each "item" is an opaque entry with id, icon, label, and an onUse callback.
|
||||
* The carousel doesn't know about shop items or actions — the parent maps them.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
@@ -58,7 +57,7 @@ export function ItemCarousel({
|
||||
|
||||
if (count === 0) {
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center py-4', className)}>
|
||||
<div className={cn('flex items-center justify-center py-3', className)}>
|
||||
<p className="text-xs text-muted-foreground/50">Nothing here yet</p>
|
||||
</div>
|
||||
);
|
||||
@@ -68,15 +67,16 @@ export function ItemCarousel({
|
||||
const prevItem = items[(index - 1 + count) % count];
|
||||
const nextItem = items[(index + 1) % count];
|
||||
const isThisActive = activeItemId === current.id;
|
||||
const showPreviews = count >= 3;
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center gap-1', className)}>
|
||||
<div className={cn('flex items-center justify-center', className)}>
|
||||
{/* Left arrow */}
|
||||
<button
|
||||
onClick={prev}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'size-8 rounded-full flex items-center justify-center shrink-0',
|
||||
'size-7 sm:size-8 rounded-full flex items-center justify-center shrink-0',
|
||||
'text-muted-foreground/40 hover:text-foreground/70 hover:bg-accent/40',
|
||||
'transition-all duration-200 active:scale-90',
|
||||
disabled && 'opacity-30 pointer-events-none',
|
||||
@@ -86,9 +86,9 @@ export function ItemCarousel({
|
||||
<ChevronLeft className="size-4" />
|
||||
</button>
|
||||
|
||||
{/* Preview (prev) — only when 3+ items */}
|
||||
{count >= 3 && (
|
||||
<div className="flex flex-col items-center opacity-30 scale-75 shrink-0 w-12 pointer-events-none select-none">
|
||||
{/* Preview (prev) — desktop only, 3+ items */}
|
||||
{showPreviews && (
|
||||
<div className="hidden sm:flex flex-col items-center opacity-25 scale-[0.65] shrink-0 w-10 pointer-events-none select-none">
|
||||
<span className="text-2xl leading-none">{prevItem.icon}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -98,24 +98,24 @@ export function ItemCarousel({
|
||||
onClick={() => onUse(current.id)}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'relative flex flex-col items-center gap-1 py-2 px-4 rounded-2xl transition-all duration-200 shrink-0',
|
||||
'relative flex flex-col items-center gap-0.5 py-1 px-3 sm:px-4 rounded-2xl transition-all duration-200 shrink-0',
|
||||
'hover:-translate-y-0.5 active:scale-95',
|
||||
isThisActive && 'bg-accent/40',
|
||||
disabled && !isThisActive && 'opacity-50 pointer-events-none',
|
||||
)}
|
||||
>
|
||||
<span className="text-5xl sm:text-6xl leading-none transition-transform duration-200 hover:scale-110">
|
||||
<span className="text-4xl sm:text-5xl leading-none transition-transform duration-200 hover:scale-110">
|
||||
{current.icon}
|
||||
</span>
|
||||
<span className="text-xs font-medium text-foreground/70">{current.label}</span>
|
||||
<span className="text-[10px] sm:text-xs font-medium text-foreground/70">{current.label}</span>
|
||||
{isThisActive && (
|
||||
<Loader2 className="size-4 animate-spin text-primary absolute -bottom-0.5" />
|
||||
<Loader2 className="size-3.5 animate-spin text-primary absolute -bottom-1" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Preview (next) — only when 3+ items */}
|
||||
{count >= 3 && (
|
||||
<div className="flex flex-col items-center opacity-30 scale-75 shrink-0 w-12 pointer-events-none select-none">
|
||||
{/* Preview (next) — desktop only, 3+ items */}
|
||||
{showPreviews && (
|
||||
<div className="hidden sm:flex flex-col items-center opacity-25 scale-[0.65] shrink-0 w-10 pointer-events-none select-none">
|
||||
<span className="text-2xl leading-none">{nextItem.icon}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -125,7 +125,7 @@ export function ItemCarousel({
|
||||
onClick={next}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'size-8 rounded-full flex items-center justify-center shrink-0',
|
||||
'size-7 sm:size-8 rounded-full flex items-center justify-center shrink-0',
|
||||
'text-muted-foreground/40 hover:text-foreground/70 hover:bg-accent/40',
|
||||
'transition-all duration-200 active:scale-90',
|
||||
disabled && 'opacity-30 pointer-events-none',
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
/**
|
||||
* RoomActionButton — Unified circular action button for all rooms.
|
||||
*
|
||||
* Matches the visual language of the original Photo and Companion buttons:
|
||||
* - Large rounded-full circle with soft radial glow background
|
||||
* - Icon centred inside
|
||||
* - Label beneath
|
||||
* - Hover lift + scale, active scale-down
|
||||
* - Consistent size across all rooms
|
||||
* Responsive sizing:
|
||||
* - Mobile: size-14 circle, size-7 icons
|
||||
* - Desktop (sm+): size-20 circle, size-9 icons
|
||||
*
|
||||
* Matches the soft radial glow of the original Photo / Companion buttons
|
||||
* but at a smaller scale so the bottom bar feels proportional on mobile.
|
||||
*/
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
@@ -48,7 +48,7 @@ export function RoomActionButton({
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'flex flex-col items-center gap-1.5 transition-all duration-300 ease-out',
|
||||
'flex flex-col items-center gap-1 transition-all duration-300 ease-out shrink-0',
|
||||
'hover:-translate-y-1 hover:scale-110 active:scale-95',
|
||||
disabled && 'opacity-50 pointer-events-none',
|
||||
className,
|
||||
@@ -56,13 +56,13 @@ export function RoomActionButton({
|
||||
>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn('size-20 sm:size-24 rounded-full flex items-center justify-center', color)}
|
||||
className={cn('size-14 sm:size-20 rounded-full flex items-center justify-center', color)}
|
||||
style={{
|
||||
background: `radial-gradient(circle at 40% 35%, color-mix(in srgb, ${glowHex} 14%, transparent), color-mix(in srgb, ${glowHex} 4%, transparent) 70%)`,
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="size-9 sm:size-10 animate-spin" />
|
||||
<Loader2 className="size-7 sm:size-9 animate-spin" />
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
@@ -73,7 +73,7 @@ export function RoomActionButton({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs font-medium text-muted-foreground">{label}</span>
|
||||
<span className="text-[10px] sm:text-xs font-medium text-muted-foreground">{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user