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.
- When moving: Eyes look in direction Blobbi is going (left/right)
- When idle: Eyes look around randomly, observing the screen
- Wider gaze range for more noticeable movement
- Faster gaze changes (0.8-2.5s) feel more alive
- Mouse focus: Brief glances at cursor
- 25% chance every 2-4 seconds (after 6s cooldown)
- Only lasts 1.5s, then returns to normal
- Never gets stuck on mouse
- Smooth transitions between all gaze targets
- Mouse follow: responsive (0.15 factor)
- Forward: moderate (0.1 factor)
- Random: gentle (0.06 factor)
- Cleaner state management using refs to avoid unnecessary re-renders
- Restore charming float/sway animation with layered sine waves
- Walking: lively bouncy motion with playful tilt
- Idle: dreamy calm floating like gentle breathing
- Fix eye gaze behavior:
- Eyes now move randomly when idle (not stuck)
- Occasional brief mouse following that properly times out
- Look in movement direction when walking
- Fix adult form rendering:
- Pass adultType and seed to visual component
- Adults now render their actual form instead of always catti
- Keep ground contact fixes:
- SVG alignment via translateY compensation
- SVG fills container with width/height 100%
- Add debug mode infrastructure (disabled by default)
Root cause: The Blobbi SVG has ~12% empty space at the bottom of its
viewBox (body ends at Y=88 in a 0-100 viewBox). This caused the visual
body to appear elevated above the ground.
Fix: Added SVG padding compensation in the ground position calculation
rather than trying to hack it in the visual component.
Changes:
- calculateGroundY(): Add svgBottomPadding (12% of size) to push
container down so Blobbi's actual body touches ground
- calculateMovementBounds(): Same adjustment for maxY bound
- Removed visual margin hacks (marginBottom, items-end) that were
trying to compensate in the wrong place
The ground Y calculation now accounts for the SVG's internal padding,
so the container is positioned lower and Blobbi's body correctly
touches the ground level.
The SVG viewBox has empty space at the bottom (~12% padding).
This was causing Blobbi to appear floating above the ground.
Fix:
- Changed inner container to use 'items-end' for bottom alignment
- Added negative marginBottom (-10% of size) to pull Blobbi down
- This compensates for the SVG's internal bottom padding
Shadow restored:
- Back to bottom: -4 (was 0)
- Size: 60% width, 12% height
- Blur: 2px
- Better visual separation from Blobbi
Result: Blobbi now visually sits on the ground with proper
shadow placement underneath.
Float animation fix:
- Changed from abs(sin) to (1-cos)/2 wave formula
- This creates a 0-to-1 range that regularly returns to zero (ground)
- Y offset now goes from 0 (ground contact) to -3/-4.5 (slight lift)
- Blobbi settles back to ground between float cycles
Walking animation:
- Faster cycle (~0.5s) for rhythmic bobbing
- Range: 0 to -4.5px lift
- Reduced sway (1.5px) and rotation (1.5°)
Idle animation:
- Slower cycle (~2.5s) for calm breathing
- Range: 0 to -3px lift
- Subtle sway (0.8px) and rotation (0.8°)
Shadow adjustments:
- Moved to bottom: 0 (right at ground level)
- Smaller size (55% width, 10% height) for subtler effect
- Less blur (1px) for sharper ground contact
- Stronger base opacity (0.4) that fades as Blobbi lifts
- Scale shrinks more noticeably when lifted
- Float animation now only moves Blobbi upward from ground level
- Uses abs(sin) so Y offset is always negative (up) or zero
- Base position = on the ground, animation lifts slightly above
- Shadow stays anchored to ground while Blobbi floats above it
- Shadow doesn't move with float offset
- Shadow scales/fades based on float height for depth illusion
- Reduced horizontal sway and rotation for subtler effect
- Walking: 2px sway, 2° rotation (was 3px, 3°)
- Idle: 1px sway, 1° rotation (was 1.5px, 1.5°)
- Simplify entry animation to smooth walking emergence (no stuck/squeeze)
- Add forced initial walk after entry - Blobbi walks right immediately
- Improve walking behavior - 75% walk chance, shorter idle periods
- Remove visual flip when changing direction - Blobbi always faces same way
- Add soft floating/swaying animation with different speeds for walk vs idle
- Walking: faster rhythmic bobbing (~0.8s cycle)
- Idle: slower calm breathing (~3s cycle)
- Add soft shadow underneath for depth/floating effect
- Stronger opacity (0.35), blur, and gradient for better visibility
- Shadow reacts to float height
- Keep clipping behavior for sidebar emergence on desktop
- Mobile uses simple slide-in from left edge
Entry position changes:
- Add layout config with sidebarWidth (300px) and maxContentWidth (1200px)
- Calculate main content left edge accounting for centered layout
- Entry now starts at left edge of main content area, not viewport edge
- Resting position is inside the content area with proper padding
Playful entry animation (2.2 seconds total):
- Phase 1 (0-25%): Emerge diagonally with slight forward lean and squish
- Phase 2 (25-40%): Get 'stuck' halfway with wobble effect
- Phase 3 (40-70%): Tug motions - 3 cycles of forward/back pulls, each stronger
- Phase 4 (70-100%): Break free and walk smoothly to final position
Visual effects during entry:
- Rotation (lean forward/back during tugging)
- ScaleX/ScaleY (squish/stretch for squeeze effect)
- Transform origin at center bottom for natural pivoting
The animation feels like Blobbi is squeezing out from the previous page
into the current one, getting briefly stuck, then breaking free.
- Change entry position to start behind the sidebar (padding.left/2 - size)
instead of off-screen left edge, so companion emerges from sidebar
- Add setPosition function to motion hook for syncing position
- Sync motion position to restingPosition when entry animation completes
to prevent teleport between animated and physics-controlled movement
- Entry is now continuous: emerges from sidebar -> slides to resting position
- Add canBeCompanion check to prevent eggs from being set as companion
- Show toast error if user tries to set egg as companion
- Disable companion button for eggs with helpful tooltip
- Update tooltip to show 'Hatch first to set as companion' for eggs
- Remove current_companion tag entirely when unsetting (instead of empty string)
- Filter out existing current_companion tags before adding new one
- Add Footprints icon indicator in BlobbiSelectorCard for current companion
- Include tooltip 'Current companion' on the indicator icon
- Pass currentCompanion prop through BlobbiSelectorPage and modal
- Add isCurrentCompanion and isUpdatingCompanion props to BlobbiDashboardFloatingControls
- Implement handleSetAsCompanion to toggle current_companion tag on profile
- Show green icon color when Blobbi is the current companion
- Add disabled state support to FloatingActionDef for loading states
- Pass publishEvent to BlobbiDashboard for profile updates
- Daily Missions section is now collapsible with chevron toggle
- Hatch Tasks section is collapsible when active
- Evolve Tasks section is collapsible when active
- All sections expanded by default for easy access
- Header shows progress count (e.g., 2/4) for task sections
- Header shows coins earned for Daily Missions section
- Smooth chevron rotation animation on expand/collapse
Apply the same pattern used in other Blobbi modals:
- Sticky header with bg-background
- Explicit DialogClose button in header
- Scrollable content area with flex-1 min-h-0 overflow-y-auto
- Add pr-12 to DialogHeader to account for close button, fixing right padding
- Make DialogHeader sticky with proper background for all Blobbi modals
- Structure modals as flex column with min-h-0 for proper scrolling
- Apply pattern to: BlobbiActionsModal, BlobbiActionInventoryModal,
BlobbiInventoryModal, BlobbiMissionsModal, BlobbiShopModal