diff --git a/package-lock.json b/package-lock.json index 630706fa..3ebc70f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -109,7 +109,7 @@ "html-to-image": "^1.11.13", "idb": "^8.0.3", "input-otp": "^1.2.4", - "lucide-react": "^0.462.0", + "lucide-react": "^1.8.0", "nostr-tools": "^2.13.0", "qrcode": "^1.5.4", "react": "^19.2.4", @@ -5758,6 +5758,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5771,6 +5772,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5784,6 +5786,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5797,6 +5800,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5810,6 +5814,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5823,6 +5828,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5836,6 +5842,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5849,6 +5856,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5862,6 +5870,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5875,6 +5884,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5888,6 +5898,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5901,6 +5912,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5914,6 +5926,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5927,6 +5940,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5940,6 +5954,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5953,6 +5968,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5966,6 +5982,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5979,6 +5996,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5992,6 +6010,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6005,6 +6024,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6018,6 +6038,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6031,6 +6052,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6044,6 +6066,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6057,6 +6080,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6070,6 +6094,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10087,12 +10112,12 @@ "license": "ISC" }, "node_modules/lucide-react": { - "version": "0.462.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.462.0.tgz", - "integrity": "sha512-NTL7EbAao9IFtuSivSZgrAh4fZd09Lr+6MTkqIxuHaH2nnYiYIzXPo06cOxHg9wKLdj6LL8TByG4qpePqwgx/g==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz", + "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==", "license": "ISC", "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/lz-string": { diff --git a/package.json b/package.json index a4162e7a..c53c9002 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "html-to-image": "^1.11.13", "idb": "^8.0.3", "input-otp": "^1.2.4", - "lucide-react": "^0.462.0", + "lucide-react": "^1.8.0", "nostr-tools": "^2.13.0", "qrcode": "^1.5.4", "react": "^19.2.4", diff --git a/src/blobbi/rooms/components/BlobbiRoomHero.tsx b/src/blobbi/rooms/components/BlobbiRoomHero.tsx index d63d777d..c648b630 100644 --- a/src/blobbi/rooms/components/BlobbiRoomHero.tsx +++ b/src/blobbi/rooms/components/BlobbiRoomHero.tsx @@ -18,6 +18,8 @@ import type { BlobbiCompanion } from '@/blobbi/core/lib/blobbi'; import type { BlobbiEmotion } from '@/blobbi/ui/lib/emotion-types'; import type { BlobbiVisualRecipe } from '@/blobbi/ui/lib/recipe'; import type { BlobbiReactionState } from '@/blobbi/actions'; +import type { BlobbiRoomId } from '../lib/room-config'; +import { ROOM_META, DEFAULT_ROOM_ORDER, getRoomIndex } from '../lib/room-config'; import { cn } from '@/lib/utils'; // ─── Stat colour maps ───────────────────────────────────────────────────────── @@ -72,6 +74,10 @@ export interface BlobbiRoomHeroProps { handleSetAsCompanion: () => Promise; heroRef: React.RefObject; heroWidth: number; + /** Current room (for indicator below name) */ + roomId: BlobbiRoomId; + /** Room order for dot indicators */ + roomOrder?: BlobbiRoomId[]; className?: string; } @@ -92,8 +98,12 @@ export function BlobbiRoomHero({ handleSetAsCompanion, heroRef, heroWidth, + roomId, + roomOrder = DEFAULT_ROOM_ORDER, className, }: BlobbiRoomHeroProps) { + const roomMeta = ROOM_META[roomId]; + const roomIndex = getRoomIndex(roomId, roomOrder); if (isActiveFloatingCompanion) { return (
@@ -159,6 +169,25 @@ export function BlobbiRoomHero({ {companion.name} )} + + {/* Room indicator */} +
+
+ + {roomMeta.label} +
+
+ {roomOrder.map((id, i) => ( +
+ ))} +
+
); diff --git a/src/blobbi/rooms/components/BlobbiRoomShell.tsx b/src/blobbi/rooms/components/BlobbiRoomShell.tsx index 09d4b9d4..7b161201 100644 --- a/src/blobbi/rooms/components/BlobbiRoomShell.tsx +++ b/src/blobbi/rooms/components/BlobbiRoomShell.tsx @@ -6,9 +6,8 @@ * The parent decides what bottom bar to render based on the active room. */ -import { useState, useCallback, useMemo, useEffect, type CSSProperties } from 'react'; +import { useState, useCallback, useMemo, useEffect, useRef as useReactRef, type CSSProperties } from 'react'; import { ChevronLeft, ChevronRight } from 'lucide-react'; -import { useIsMobile } from '@/hooks/useIsMobile'; import { cn } from '@/lib/utils'; import { toast } from '@/hooks/useToast'; @@ -18,7 +17,6 @@ import { DEFAULT_ROOM_ORDER, getNextRoom, getPreviousRoom, - getRoomIndex, } from '../lib/room-config'; import { generateInitialPoops, @@ -59,6 +57,9 @@ interface BlobbiRoomShellProps { // ─── Component ──────────────────────────────────────────────────────────────── +/** Minimum horizontal swipe distance (px) to trigger room change */ +const SWIPE_THRESHOLD = 50; + export function BlobbiRoomShell({ roomId, onChangeRoom, @@ -71,10 +72,6 @@ export function BlobbiRoomShell({ lastFeedTimestamp, poopStateRef, }: BlobbiRoomShellProps) { - const isMobile = useIsMobile(); - const meta = ROOM_META[roomId]; - const roomIndex = getRoomIndex(roomId, roomOrder); - const goLeft = useCallback(() => { onChangeRoom(getPreviousRoom(roomId, roomOrder)); }, [roomId, roomOrder, onChangeRoom]); @@ -83,15 +80,25 @@ export function BlobbiRoomShell({ onChangeRoom(getNextRoom(roomId, roomOrder)); }, [roomId, roomOrder, onChangeRoom]); - const dots = useMemo(() => roomOrder.map((id, i) => ({ - id, - active: i === roomIndex, - label: ROOM_META[id].label, - })), [roomOrder, roomIndex]); - const leftDest = ROOM_META[getPreviousRoom(roomId, roomOrder)]; const rightDest = ROOM_META[getNextRoom(roomId, roomOrder)]; + // ─── Touch swipe ─── + const touchStartX = useReactRef(null); + + const onTouchStart = useCallback((e: React.TouchEvent) => { + touchStartX.current = e.touches[0].clientX; + }, []); + + const onTouchEnd = useCallback((e: React.TouchEvent) => { + if (touchStartX.current === null) return; + const dx = e.changedTouches[0].clientX - touchStartX.current; + touchStartX.current = null; + if (Math.abs(dx) < SWIPE_THRESHOLD) return; + if (dx > 0) goLeft(); + else goRight(); + }, [goLeft, goRight]); + // ─── Poop system (ephemeral) ─── const [poops, setPoops] = useState([]); const [shovelMode, setShovelMode] = useState(false); @@ -117,11 +124,14 @@ export function BlobbiRoomShell({ poops, shovelMode, setShovelMode, onRemovePoop, }), [poops, shovelMode, onRemovePoop]); - // Expose poop state to parent so bottom bars can access it if (poopStateRef) poopStateRef.current = poopState; return ( -
+
{/* Room content */}
{hero} @@ -137,71 +147,41 @@ export function BlobbiRoomShell({ /> )} - {/* Room header */} -
-
-
- {meta.icon} - {meta.label} -
-
- {dots.map(dot => ( -
- ))} -
-
-
- {/* Navigation arrows */} diff --git a/src/blobbi/rooms/lib/room-config.ts b/src/blobbi/rooms/lib/room-config.ts index 436e6876..d0a5085b 100644 --- a/src/blobbi/rooms/lib/room-config.ts +++ b/src/blobbi/rooms/lib/room-config.ts @@ -5,6 +5,8 @@ * The current room is persisted as a tag on kind 11125. */ +import { Home, Refrigerator, Cross, Moon, Shirt, type LucideIcon } from 'lucide-react'; + // ─── Room IDs ───────────────────────────────────────────────────────────────── export type BlobbiRoomId = 'home' | 'kitchen' | 'care' | 'rest' | 'closet'; @@ -15,7 +17,7 @@ export interface BlobbiRoomMeta { id: BlobbiRoomId; label: string; description: string; - icon: string; + icon: LucideIcon; } export const ROOM_META: Record = { @@ -23,31 +25,31 @@ export const ROOM_META: Record = { id: 'home', label: 'Home', description: 'Main living room', - icon: '🏠', + icon: Home, }, kitchen: { id: 'kitchen', label: 'Kitchen', description: 'Feed your Blobbi', - icon: '🍳', + icon: Refrigerator, }, care: { id: 'care', label: 'Care Room', description: 'Hygiene, care, and medicine', - icon: '🩹', + icon: Cross, }, rest: { id: 'rest', label: 'Bedroom', description: 'Rest and recharge', - icon: '🌙', + icon: Moon, }, closet: { id: 'closet', label: 'Closet', description: 'Wardrobe and accessories', - icon: '👗', + icon: Shirt, }, }; diff --git a/src/index.css b/src/index.css index e9456a9a..f2e8edde 100644 --- a/src/index.css +++ b/src/index.css @@ -699,3 +699,14 @@ 100% { opacity: 1; } } +/* Room navigation arrow nudge — subtle horizontal pulse */ +@keyframes room-arrow-nudge-left { + 0%, 100% { transform: translateX(0); } + 50% { transform: translateX(-3px); } +} + +@keyframes room-arrow-nudge-right { + 0%, 100% { transform: translateX(0); } + 50% { transform: translateX(3px); } +} + diff --git a/src/pages/BlobbiPage.tsx b/src/pages/BlobbiPage.tsx index a6a3ba49..af243de8 100644 --- a/src/pages/BlobbiPage.tsx +++ b/src/pages/BlobbiPage.tsx @@ -3,7 +3,7 @@ import { createPortal } from 'react-dom'; import { Link, useNavigate } from 'react-router-dom'; import { useSeoMeta } from '@unhead/react'; import { nip19 } from 'nostr-tools'; -import { Egg, Moon, Sun, RefreshCw, Check, Plus, Camera, AlertTriangle, Footprints, Wrench, Theater, ExternalLink, Utensils, Gamepad2, Sparkles, Pill, Music, Mic, Loader2, Target, Droplets, Heart, Zap } from 'lucide-react'; +import { Egg, Moon, Sun, RefreshCw, Check, Plus, Camera, AlertTriangle, Footprints, Wrench, Theater, ExternalLink, Utensils, Gamepad2, Sparkles, Pill, Music, Mic, Loader2, Target, Droplets, Heart, Zap, Refrigerator, ShowerHead, Candy, Shovel, TowelRack } from 'lucide-react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useProjectedBlobbiState } from '@/blobbi/core/hooks/useProjectedBlobbiState'; @@ -1496,6 +1496,7 @@ function BlobbiDashboard({ handleSetAsCompanion={handleSetAsCompanion} heroRef={heroRef} heroWidth={heroWidth} + roomId={currentRoom} /> } middleSlot={ @@ -1818,7 +1819,7 @@ function KitchenBar({
{anyPoop && poopState ? ( 🧹} + icon={} label={poopState.shovelMode ? 'Done' : 'Shovel'} color={poopState.shovelMode ? 'text-amber-600' : 'text-stone-500'} glowHex={poopState.shovelMode ? '#d97706' : '#78716c'} @@ -1837,7 +1838,7 @@ function KitchenBar({ />
🧊} + icon={} label="Fridge" color="text-orange-500" glowHex="#f97316" @@ -1897,7 +1898,7 @@ function CareBar({ {isHygieneFocused ? ( towelItem ? ( {towelItem.icon}} + icon={} label="Towel" color="text-cyan-500" glowHex="#06b6d4" @@ -1910,7 +1911,7 @@ function CareBar({ ) ) : ( 🍬} + icon={} label="Treat" color="text-pink-400" glowHex="#f472b6" @@ -1932,7 +1933,7 @@ function CareBar({
{isHygieneFocused ? ( 🚿} + icon={} label="Shower" color="text-blue-500" glowHex="#3b82f6"