Action Menu Fixes:
- Remove useMemo for position calculations to avoid stale values
- Calculate button positions directly each render
- Menu now follows Blobbi continuously during all states
(idle, walking, floating, dragging, settling)
Hanging Items System:
- New HangingItems component replaces CompanionItemBubbles
- Items appear as circles hanging from vertical lines at top of screen
- Wider horizontal spacing (80px between items)
- Playful, spatial presentation instead of modal-like container
Click-to-Fall Animation:
- Clicking an item releases it from the hanger
- Line and quantity badge disappear instantly
- Item falls with rotation animation (800ms)
- Structured for future extension (drag, attraction, reactions)
Pointer Events:
- Container uses pointer-events-none
- Individual items use pointer-events-auto
- All items are now properly clickable
Removed:
- CompanionItemBubbles component (deleted)
- Modal-like container presentation
- Close button (no longer needed)
- Expose rendered position from BlobbiCompanion via onPositionUpdate callback
- Track actual visual position in BlobbiCompanionLayer (includes entry animation + float offset)
- Pass rendered position to CompanionActionMenu instead of logical motion.position
- Add pointer-events-auto to action menu backdrop, buttons, and item bubbles
- Fix pointer events hierarchy (parent layer has pointer-events-none)
- Fix unused variable lint errors (entryProgress, isPermanentlyStuck, etc.)
Implement the first interaction layer for the Blobbi companion:
Click vs Drag detection:
- Created useClickDetection hook to distinguish clicks from drags
- Movement threshold: 5px (beyond = drag)
- Time threshold: 300ms (beyond = not a click)
- Updated BlobbiCompanion pointer/touch handlers to use detection
Action Menu (CompanionActionMenu):
- Radial/arc layout centered above Blobbi
- 5 actions: feed, play, medicine, clean, sleep
- Stage-aware: eggs only see medicine and clean
- Smooth fade-in + zoom animation with stagger
- Click outside closes menu
- Route change closes menu
Item Bubbles (CompanionItemBubbles):
- Horizontal row of item bubbles near top of screen
- Shows emoji + quantity badge for each item
- Resolves real inventory items for selected action
- Empty state message when no items available
- Staggered appearance animation
Architecture:
- /interaction/types.ts - Type definitions and config
- /interaction/useCompanionActionMenu.ts - Menu state hook
- /interaction/useClickDetection.ts - Click/drag detection
- /interaction/CompanionActionMenu.tsx - Radial menu component
- /interaction/CompanionItemBubbles.tsx - Item display component
- /interaction/index.ts - Module exports
Action to item category mapping:
- feed -> food items
- play -> toy items
- medicine -> medicine items
- clean -> hygiene items
- sleep -> (no items, direct action)
Future-ready for:
- Item falling animation
- Drag item to Blobbi
- Blobbi walking toward items
- Item consumption logic
- Per-item Blobbi reactions
Caret tracking implementation (priority order):
1. contenteditable: window.getSelection() + Range.getBoundingClientRect()
2. input/textarea: selectionStart + mirrored text measurement
3. Fallback: right-side typing region (where new text appears)
4. Last resort: field center
Changes from field-center version:
- Added getContentEditableCaretPosition() using Selection API
- Added getInputCaretPosition() with text width measurement via temp span
- Added getRightTypingRegion() as smart fallback (better than center)
- computeCaretPosition() tries each method in priority order
Event handling (event-driven, no polling):
- keydown: detect typing, update caret after DOM updates via rAF
- input: catch paste, autocomplete, non-keydown text changes
- selectionchange: update when caret moves via arrow keys or click
- focusin/focusout: track element changes, clean up on blur
Timing:
- Increased idle timeout from 2s to 4s for more stable observation
- Timeout resets on any typing event
Priority:
- Typing attention now uses 'high' priority
- Overrides generic modal attention while typing
- Keeps Blobbi focused on caret, not just modal center
Handles edge cases:
- Focus leaving field clears typing attention
- Modal close clears via overlay detection
- Switching fields updates target to new field's caret
- Graceful fallback when exact caret rect unavailable
- Caret position clamped to element bounds
When user types in a text input inside a modal/dialog, Blobbi now:
- Detects focus on text inputs (input, textarea, contenteditable, role=textbox)
- Only activates when the field is inside an overlay (modal, dialog, sheet, drawer)
- Locks attention to the focused field's center while typing continues
- Releases attention after 2s idle timeout (no typing)
- Properly cleans up when focus changes or modal closes
Implementation:
- New useTypingAttention hook handles focus/blur/keydown events
- Integrated into useBlobbiAttention with priority below 'high' but above 'low'
- Typing attention overrides random gaze and mouse-follow during active typing
- Lightweight: uses event listeners, not polling; targets field center, not caret
Config:
- Added typingIdleTimeout (2000ms) to attention config
Edge cases handled:
- Focus moving between text fields updates attention target
- Focus leaving text inputs clears typing attention
- Modal close clears typing attention via overlay detection
- Only typing keys (chars, backspace, delete, enter) reset idle timer
Root cause: useBlobbiCompanionData had a separate query key (['companion-profile'])
from useBlobbonautProfile (['blobbonaut-profile']). When companion was
selected/removed via BlobbiPage, only the main profile query was invalidated,
leaving the companion layer with stale data.
Fix:
- Rewrote useBlobbiCompanionData to use useBlobbonautProfile instead of
duplicating the profile query
- Now shares the same query cache, so profile updates (including currentCompanion
changes) immediately propagate to the companion layer
- Added explicit null return when currentCompanionD is undefined for reactive removal
BlobbiPage duplicate prevention:
- Added check for active floating companion (isActiveFloatingCompanion)
- When the displayed Blobbi is the same as the floating companion, show a
friendly message: '{name} is out exploring right now.' with Footprints icon
- Prevents seeing two identical Blobbis (one floating, one in-page)
This makes companion selection/removal/replacement fully reactive without
requiring page refresh.
- Track companionId in useBlobbiEntryAnimation to detect companion changes
- When companion is selected/changed, trigger new entry animation immediately
- Random entry direction (fall or rise) for new companion appearances
- No transition delay for companion changes (unlike route changes)
- Companion removal hides Blobbi via existing isVisible logic
Root cause:
When entry animation completed, there was a race condition:
1. entryState.phase becomes 'complete'
2. Effect calls completeEntry() which sets isEntering = false
3. Component checks 'if (isEntering)' and switches to motion.position
4. But motion.position wasn't synced to groundPosition yet
5. Result: Blobbi snaps to old position instead of falling smoothly
The fix uses a two-phase handoff:
1. Component now checks 'isEntering || entryState.phase !== idle'
- Keeps using entry animation position during 'complete' phase
- The 'complete' phase returns groundPosition, so rendering is correct
2. Added acknowledgeCompletion() function
- Called after position is synced AND rendered (via requestAnimationFrame)
- Resets phase to 'idle' to allow normal motion to take over
- Ensures no frame shows wrong position
3. Position sync effect now:
- Detects phase === 'complete' (not just !isEntering)
- Syncs position to groundPosition
- Waits one frame with requestAnimationFrame
- Then calls acknowledgeCompletion() to handoff to motion
Files changed:
- useBlobbiEntryAnimation.ts: Add acknowledgeCompletion()
- useBlobbiCompanion.ts: Use acknowledgeCompletion after sync
- BlobbiCompanion.tsx: Check phase !== 'idle' for entry position
FALL entry changes:
- Simplified to 2 pull attempts (was wiggle-based)
- Each pull: quick down, slower up (purely vertical, no diagonal)
- Normal flow (~80%): stuck -> pull_1 -> pause_1 -> pull_2 -> pause_2 -> fall -> land
- Rare flow (~20%): stuck -> pull_1 -> pause_1 -> pull_2 -> stuck_permanent
Permanent stuck behavior:
- 20% chance on FALL entry (configurable via trulyStuckChance)
- Blobbi hangs at top, won't fall automatically
- User must drag and release to rescue
- Resolves when drag ends (after any movement away from stuck position)
Route change handling:
- Cancels current entry immediately (no continuation)
- Waits 1 second after new page appears
- Then restarts entry animation for the new route
Timing (total ~1470ms for normal fall):
- stuck: 200ms (15% visible)
- pull_1: 280ms (10% drop)
- pause_1: 140ms
- pull_2: 300ms (14% drop)
- pause_2: 100ms
- fall: 450ms
- land: 200ms
Files changed:
- companion.types.ts: New phases (pulling_1/2, pause_1/2, stuck_permanent), isTrulyStuck flag
- companionConfig.ts: New timing config for 2-pull system
- animation.ts: Simplified vertical pull calculations
- useBlobbiEntryAnimation.ts: New state machine with 20% stuck chance
- useBlobbiCompanion.ts: Pass isDragging to entry hook
- BlobbiCompanion.tsx: Updated config mapping
- Add 'stuck' phase where Blobbi's butt hangs from top edge (25% visible)
- Add 'wiggling' phase with left-right wiggle (±8px, ±6° rotation) to get loose
- Update entry state machine to handle new phases: stuck -> wiggling -> falling -> landing
- Add config values for stuckDuration (300ms) and wiggleDuration (400ms)
- Update animation utils with wiggle offset calculations
- Add vertical entry system: fall from top when navigating DOWN sidebar,
rise from bottom with inspection when navigating UP sidebar
- Add sidebarNavigation.ts utility to map routes to sidebar order
- Remove old sidebar-based entry (clipping, peeking from left)
- Fix motion sync to use groundPosition (center) instead of old restingPosition
- Entry now ends at center of screen, no teleport on completion
Tab switch attention:
- Added TAB_SELECTORS for Radix UI tabs with data-state='active'
- Tab changes trigger brief glance (1.2s) instead of full attention (3s)
- Uses shorter glanceCooldown (0.8s) to allow noticing multiple tabs
- Low priority so overlays can still interrupt
- Skipped tooltips entirely (too noisy)
Post-route attention:
- After entry animation completes, Blobbi looks at main content
- findMainContentPosition() tries common selectors: main, [role=main], .main-content, article
- Falls back to center-top of viewport if no main content found
- Uses postRouteDuration (2.5s) for how long to look
- Small postRouteDelay (200ms) before triggering
- Low priority so modals can immediately interrupt
Improved upward pupil movement:
- Asymmetric vertical scaling in BlobbiBabyVisual and BlobbiAdultVisual
- Looking up: full range (4px/4.5px for baby/adult)
- Looking down: reduced range (2.4px/2.7px, 0.6x) to avoid droopy look
- Updated calculateEyeOffset() with asymmetric maxDistanceY:
- 350px for looking up (easier to reach full upward gaze)
- 500px for looking down (normal distance)
- Updated generateRandomScreenGaze() to favor upward looks (-0.6 to +0.5)
New config options:
- attention.glanceDuration: 1200ms for brief tab glances
- attention.glanceCooldown: 800ms between glances
- attention.postRouteDuration: 2500ms for post-route attention
- attention.postRouteDelay: 200ms delay before post-route attention
Files changed:
- companion.types.ts: New config options
- companionConfig.ts: New timing values
- companionMachine.ts: Asymmetric calculateEyeOffset
- useBlobbiAttention.ts: Tab detection, overlay/tab separation
- useBlobbiCompanion.ts: Post-route attention trigger
- useBlobbiCompanionGaze.ts: Improved random gaze range
- BlobbiBabyVisual.tsx: Asymmetric vertical eye movement
- BlobbiAdultVisual.tsx: Asymmetric vertical eye movement
Behavioral rebalancing:
- Reduced walk chance from 75% to 30% for much calmer behavior
- Increased idle time from 2-6s to 4-10s
- Increased random gaze interval from 0.8-2.5s to 1.5-4s for more deliberate observation
- Mouse follow now more frequent (35% vs 25%), longer duration (2.5s vs 1.5s)
- Observation look duration increased from 2-4s to 3-6s
New attention system for UI changes:
- Added useBlobbiAttention hook that watches for new UI elements
- Uses MutationObserver to detect modals, dialogs, sheets, popovers appearing
- Supports Radix UI, Vaul drawer, and common dialog patterns
- New 'attending' state has highest priority, interrupts walking
- Blobbi stops and looks at new UI element for ~3 seconds
- Priority system (low/normal/high) prevents spamming
- Cooldown prevents excessive reactions (1.5s minimum between events)
Architecture for future extensibility:
- AttentionTarget type with id, position, duration, priority, source
- AttentionPriority type for behavior hierarchy
- Exported via module index for external use
- Easy to add video/audio/notification attention types later
Files changed:
- companion.types.ts: Added 'attending' state, 'attend-ui' gaze mode, AttentionTarget type
- companionConfig.ts: Rebalanced all timing values, added attention config
- companionMachine.ts: Reduced walk chance, handle attending in motion
- useBlobbiAttention.ts: New hook for UI attention detection
- useBlobbiCompanionState.ts: Handle attending state, save/restore behavior
- useBlobbiCompanionGaze.ts: Handle attend-ui gaze mode with fast snap
- useBlobbiCompanion.ts: Wire attention system through
- index.ts: Export new hook and types
- Fix competing eye systems: add disableTracking option to useBlobbiEyes
so external systems can control eye position while keeping blinking
- Add externalEyeOffset prop to BlobbiBabyVisual and BlobbiAdultVisual
for direct companion control of eye position
- Increase pupil movement visibility (4px for baby, 4.5px for adult)
- Increase movement direction gaze offset (0.85 instead of 0.7)
- Add observation target behavior: Blobbi picks a random screen position,
walks toward it, then looks at it for 2-4 seconds
- Add new 'observe-target' gaze mode and 'watching' state
- Wire eyeOffset through companion pipeline to visual components
- Clear separation of concerns: useBlobbiEyes handles blinking,
companion system handles gaze direction
Replace the compact ExternalPostCard with a full feed-style layout
matching the /bluesky feed when viewing individual Bluesky posts.
Shows large avatar, full text, image/link embeds, and interactive
action buttons (comment, repost, react, share) for a thread-like feel.
Thread hideImage prop through LinkEmbed -> BlueskyEmbed -> ExternalPostCard
so images are suppressed in the compose dialog preview, matching the
behavior of normal post reply indicators.
Clicking the avatar, display name, or handle now navigates to
/i/{bsky.app/profile/handle} instead of the post's /i/ page.
Clicks stop propagation so the card-level navigation still works.
ReplyComposeModal now renders a LinkPreview card when the event is a URL,
so users see what they're commenting on. Previously the preview was skipped
entirely for URL-based comments.
Pass post.likeCount to ExternalReactionButton via new count prop so like
counts display from the Bluesky API. Comment button now opens the
ReplyComposeModal with the post URL instead of navigating away.
The Bluesky page had a broken inline reaction that just published '+' with
no emoji picker. The /i/ page had the full implementation with Popover,
QuickReactMenu, hover-to-open, and custom emoji support. Extracted the
reaction logic into a reusable ExternalReactionButton component and wired
it into both pages, removing ~230 lines of duplicated/inferior code.