Commit Graph

2405 Commits

Author SHA1 Message Date
filemon c659eaeead Replace item bubbles with hanging items, fix action menu tracking
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)
2026-03-24 20:07:16 -03:00
filemon 37c7a37bdf Fix companion menu positioning and pointer-events
- 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.)
2026-03-24 19:56:34 -03:00
filemon d3f23544cc Add companion action menu and item bubbles interaction layer
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
2026-03-24 19:44:48 -03:00
filemon 9d5ea22806 Upgrade typing attention to caret-aware targeting with 4s timeout
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
2026-03-24 12:32:08 -03:00
filemon 1388d0e514 Add typing attention behavior for Blobbi in modal text fields
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
2026-03-24 12:24:20 -03:00
filemon e3412fac46 Merge branch 'main' into feat-blobbi 2026-03-24 12:20:40 -03:00
filemon e43c0b1e2e Fix reactive companion visibility and add BlobbiPage duplicate prevention
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.
2026-03-24 12:19:00 -03:00
filemon b13a5ae1ae Add reactive companion visibility with companionId tracking
- 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
2026-03-24 12:09:12 -03:00
filemon 8c52848212 Fix intermittent snap-to-ground bug in FALL entry animation
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
2026-03-24 12:05:28 -03:00
filemon 053007e7ea Fix route change: hide Blobbi immediately, delay only new entry
Before: Blobbi stayed visible during 1s delay, then disappeared and restarted
After: Blobbi disappears immediately, 1s clean delay, then new entry starts

Changes:
- Add isHiddenForTransition state to entry animation hook
- Set true immediately on route change (before delay)
- Clear when new entry actually starts
- useBlobbiCompanion now uses shouldBeVisible = isVisible && !isHiddenForTransition

Flow:
1. Route changes
2. Blobbi hidden immediately (isHiddenForTransition = true)
3. Wait 1 second (screen is clean)
4. Start new entry (isHiddenForTransition = false, entry begins)
2026-03-24 11:57:08 -03:00
filemon 9889cb07d4 Simplify FALL entry to 2 vertical pulls, add rare permanent stuck behavior
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
2026-03-24 11:46:52 -03:00
filemon 142b144318 Add pause phase to FALL entry, refine butt wiggle motion
- Add 'pause' phase (180ms) between tugging and wiggling
  Creates readable 'hmm... still stuck' beat before wiggle

- Refine wiggle to feel like lower/back part:
  - Side-to-side primary motion (not diagonal whole-body)
  - Small downward pull synced with each wiggle (tugging loose)
  - Minimal rotation (2°) - just organic, not full-body tilt
  - ~1.25 cycles instead of 1.5 (brief and playful)

- Adjusted timing:
  - wiggleIntensity: 5 → 4px
  - wiggleRotation: 3 → 2°
  - wiggleDuration: 350 → 320ms

New sequence with clear beats:
  stuck (250ms) → tugging (300ms) → pause (180ms) →
  wiggling (320ms) → falling (500ms) → landing (200ms)

Total: ~1750ms
2026-03-24 11:28:53 -03:00
filemon a47e53ff2d Refine FALL entry: add tugging phase, reduce visible amount, subtler wiggle
- Add 'tugging' phase: tries to fall but gets stuck (down-up motion)
- Reduce stuckVisibleAmount from 25% to 15% (tiny butt peek)
- Make wiggle subtler: 5px intensity, 3° rotation (was 8px, 6°)
- Wiggle now diagonal movement, not just horizontal shaking
- New sequence: stuck -> tugging -> wiggling -> falling -> landing

Timing: 250ms stuck, 300ms tug, 350ms wiggle, 500ms fall, 200ms land
2026-03-24 11:23:41 -03:00
filemon 8b88cd3cf6 Add stuck and wiggling phases to Blobbi fall entry animation
- 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
2026-03-24 11:12:33 -03:00
Chad Curtis 4ac418c78d Fix mobile bottom nav: remove spurious bottom border, fix safe-area spacer opacity 2026-03-24 09:02:57 -05:00
filemon 748bf43847 Replace sidebar entry with vertical entry based on navigation direction
- 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
2026-03-24 10:58:12 -03:00
filemon 586c536c46 up animation 2026-03-24 10:16:03 -03:00
Chad Curtis 94f5dc4b67 Show arc border on MobileTopBar when no sub-header is present 2026-03-24 07:21:01 -05:00
Chad Curtis f30d7e3668 FAB follows bottom nav, subtle shadow, fixed positioning 2026-03-24 07:11:28 -05:00
Chad Curtis 01c32a0e88 Restyle tabs and compose box: arc borders, opacity, layout order 2026-03-24 07:06:12 -05:00
Chad Curtis d5932cbb34 Replace flat tab indicator with curved arc stroke 2026-03-24 06:59:14 -05:00
Chad Curtis e8bc9a9c16 Highlight hovered tab slice on arc background 2026-03-24 06:56:49 -05:00
filemon 4a84a782db Add tab attention, post-route attention, and improve upward eye movement
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
2026-03-24 07:59:40 -03:00
filemon e2dbc0e1cf Improve Blobbi companion behavior: calmer, more observant, reactive to UI
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
2026-03-24 07:44:25 -03:00
filemon 2b434de40b Fix Blobbi companion eye behavior system
- 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
2026-03-24 07:15:19 -03:00
Chad Curtis ed86548d93 Revert "Fix 1px gap between bottom nav and safe-area spacer on iOS Safari"
This reverts commit a1cc46abc8.
2026-03-24 02:09:54 -05:00
Chad Curtis a1cc46abc8 Fix 1px gap between bottom nav and safe-area spacer on iOS Safari 2026-03-24 01:59:06 -05:00
Chad Curtis 3fb206597c Fix unused variable lint errors in MainLayout 2026-03-23 23:59:24 -05:00
Chad Curtis 47bc5f516f Fix bottom nav arc on safe-area devices: separate safe area spacer from arc container 2026-03-23 23:20:59 -05:00
Chad Curtis 1d9e445d84 Fix arc bugs: revert top arc to upstream, remove top navbar arc, separate bottom nav overhang for harsher curve 2026-03-23 23:11:29 -05:00
Alex Gleason 042489a39f Replace Saturn FAB with user's avatar shape (circle default, emoji mask when set) 2026-03-23 23:03:03 -05:00
Alex Gleason 187081199c Merge branch 'main' of gitlab.com:soapbox-pub/ditto 2026-03-23 22:56:15 -05:00
Chad Curtis 9ef6f085a0 Harsher arc curves and move bottom nav items up 2026-03-23 22:40:31 -05:00
Alex Gleason 0baeca6ce7 Render Bluesky posts as full feed-style thread on /i/ detail pages
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.
2026-03-23 22:30:45 -05:00
Alex Gleason 4c1576f704 Add Bluesky to sidebar navigation and update icon to Simple Icons SVG 2026-03-23 22:18:10 -05:00
Alex Gleason d78b8b3286 Remove category filter pills from Bluesky page 2026-03-23 22:14:57 -05:00
Alex Gleason 9947c17ddd Merge remote-tracking branch 'origin/main' into bluesky 2026-03-23 22:12:33 -05:00
Alex Gleason 4ea657a7b7 Restyle Bluesky disclaimer as a cheeky amber warning 2026-03-23 22:10:55 -05:00
Alex Gleason 715c58ceba Restore original Bluesky disclaimer text with styled callout 2026-03-23 22:10:28 -05:00
Alex Gleason 4f9f2c10bf Style Bluesky disclaimer as a branded callout banner 2026-03-23 22:09:32 -05:00
Alex Gleason d1c434050f Add Bluesky disclaimer in compose modal when replying to bsky.app URLs 2026-03-23 22:08:46 -05:00
Alex Gleason c5a9f29f60 Add infinite scroll to Bluesky feed and move attribution to info popover 2026-03-23 22:07:51 -05:00
Alex Gleason d0966b10d9 Switch Bluesky feed to official Discover (whats-hot) for curated trending content 2026-03-23 22:05:38 -05:00
Alex Gleason 645c9d427f Switch Bluesky search to actor typeahead for user discovery 2026-03-23 22:02:06 -05:00
Alex Gleason 87a295c974 Hide images in compose modal reply indicator and external post embeds
Thread hideImage prop through LinkEmbed -> BlueskyEmbed -> ExternalPostCard
so images are suppressed in the compose dialog preview, matching the
behavior of normal post reply indicators.
2026-03-23 21:57:22 -05:00
Alex Gleason 6c9a584b46 Link avatar and display name to Bluesky profile /i/ page
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.
2026-03-23 21:55:00 -05:00
Alex Gleason 0c03b78b56 Show link preview in compose modal when commenting on external URLs
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.
2026-03-23 21:52:29 -05:00
Alex Gleason dac751b0a9 Show Bluesky like counts and open comment compose dialog inline
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.
2026-03-23 21:46:14 -05:00
Alex Gleason b4eecd2588 Extract shared ExternalReactionButton with emoji picker for NIP-73 reactions
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.
2026-03-23 21:13:27 -05:00
Alex Gleason be9bef9824 Make heart button publish NIP-73 kind 17 reaction inline instead of navigating 2026-03-23 21:06:08 -05:00