Commit Graph

4447 Commits

Author SHA1 Message Date
filemon 3f32c95b35 fix(blobbi): update eye helpers for nested gaze group structure
The eye injection and detection modules were broken after adding the
nested .blobbi-eye-gaze group. The issues were:

1. modifyEyeGroupContent used indexOf('</g>') which found the gaze
   group's closing tag instead of the eye group's, breaking effects
   that modify pupil/highlight content

2. injectIntoEyeTrackLayer used a naive regex that didn't handle
   nested groups

3. detectFromProcessedSvg had a rigid regex that required exact
   class order and couldn't handle the new nested structure

Fixes:
- Added findMatchingCloseTag() for balanced group parsing
- Added findGroupByClass() helper for finding group boundaries
- Updated modifyEyeGroupContent to target .blobbi-eye-gaze (innermost)
- Updated injectIntoEyeTrackLayer to use balanced parsing
- Updated detectFromProcessedSvg with flexible class matching
- Updated documentation to reflect new 3-layer eye structure:
  1. .blobbi-blink (outer) - clip-path for eyelid animation
  2. .blobbi-eye (middle) - CSS animations like sleepy wake-glance
  3. .blobbi-eye-gaze (inner) - gaze tracking transforms

This allows eye effects (sad highlights, star eyes, etc.) to work
correctly with the new gaze/animation layer separation.
2026-03-30 19:50:44 -03:00
filemon 6192dfc568 refactor(blobbi): separate eye animation and gaze layers to prevent conflicts
Previously, both CSS animations (like sleepy wake-glance) and JS gaze
tracking targeted the same .blobbi-eye group. This caused conflicts where
external gaze had to disable CSS animations to control the transform.

Now the eye structure has three layers:
1. .blobbi-blink (outer) - clip-path for eyelid/blink animation
2. .blobbi-eye (middle) - CSS animations like sleepy wake-glance
3. .blobbi-eye-gaze (inner) - JS-controlled translate for gaze tracking

This separation allows:
- Sleepy's wake-glance CSS animation to run on .blobbi-eye
- External gaze and mouse tracking to control .blobbi-eye-gaze
- Both effects to work together without disabling either
- Eyelid clip-path animation to remain independent

Changes:
- eye-animation.ts: Added nested .blobbi-eye-gaze group inside .blobbi-eye
- eyes/types.ts: Added gazeLeft, gazeRight, gaze to EYE_CLASSES
- useBlobbiEyes.ts: Updated to target .blobbi-eye-gaze for tracking
- useExternalEyeOffset.ts: Updated to target .blobbi-eye-gaze, removed
  animation disabling hack
2026-03-30 19:19:56 -03:00
filemon de57399301 Merge branch 'main' into implement-reactions-clean 2026-03-30 19:11:31 -03:00
filemon c6e6326b50 fix(blobbi): allow external gaze to override sleepy eye animation
The sleepy emotion uses a CSS animation (sleepy-wake-glance) on .blobbi-eye
elements that applies transform: translateX() for a periodic side-glance.
Previously, useExternalEyeOffset detected this animation and yielded to it,
causing eyes to stop tracking gaze entirely during sleepy.

Now, when external gaze is active and a CSS animation is detected on eye
elements, we disable the animation and take control of the transform. This
allows:

- External gaze tracking to work during sleepy emotion
- Sleepy's eyelid closing animation (SMIL on clip-path) to continue
- The drowsy heavy-lidded effect to layer with gaze tracking

The key insight is that sleepy has two visual effects:
1. Eye position animation (CSS transform) - now disabled for external gaze
2. Eyelid closing animation (SMIL clip-path) - preserved for drowsy look
2026-03-30 19:06:49 -03:00
Alex Gleason cf59b6d0da Fix crash on /notifications from malformed badge award a-tags
Validate that the pubkey extracted from a kind 8 badge award event's
a-tag is a valid 64-char hex string before passing it to
nip19.naddrEncode(). Malformed pubkeys (from permissionless Nostr
events) caused hexToBytes() to throw 'Invalid byte sequence'.
2026-03-30 16:57:25 -05:00
filemon d836b1f068 fix(blobbi): use RAF loop in useExternalEyeOffset to prevent stuck eyes
The previous useEffect-based approach only applied eye transforms when the
externalEyeOffset prop changed. This caused eyes to get stuck in the center
when the companion was idle because:

- The gaze RAF loop updates eyeOffset state continuously
- React batches state updates, so re-renders may not happen every frame
- useBlobbiEyes also runs a RAF loop for blinking that could interfere
- SVG content changes (emotion recipes) could reset transforms

The fix uses a RAF loop that continuously applies the transform, reading
the latest offset from a ref. This ensures eyes stay positioned correctly
regardless of React render timing or SVG DOM changes.

Dragging previously 'fixed' the stuck eyes because isDragging changes
caused guaranteed re-renders that triggered the old useEffect.
2026-03-30 18:38:23 -03:00
Alex Gleason 6071a28dd9 Include all badge awards in deletion request when deleting a badge definition 2026-03-30 16:28:07 -05:00
filemon 03fa16ded2 fix(blobbi): fix companion motion/state desync and gaze RAF instability
BUGS FIXED:

1. Motion/State Desynchronization
   - useBlobbiCompanionState was receiving a hardcoded static motion object
     instead of real live motion data from useBlobbiCompanionMotion
   - This caused state decisions (walking, idle, observation) to use stale
     position/dragging data, desyncing behavior from rendered position
   - FIX: Introduce shared motionRef that motion hook writes and state hook
     reads, solving the bidirectional dependency cleanly

2. Gaze Animation Loop Instability
   - The RAF effect for smooth eye movement depended on companionPosition,
     mousePosition, observationTarget, attentionPosition, entryInspectionDirection
   - Every position change caused the loop to be torn down and recreated
   - This caused jitter during movement and stuck eyes after entry animation
   - FIX: Use refs for all frequently-changing values, only depend on isActive
     to start/stop the loop. Loop reads fresh values from refs each frame.

3. Drag Detection in State Hook
   - Changed from motion.isDragging dependency (no longer available) to
     polling motionRef.current.isDragging via interval since refs don't
     trigger re-renders

ARCHITECTURE CHANGES:
- useBlobbiCompanion: Creates shared motionRef, passes to state and motion hooks
- useBlobbiCompanionMotion: Accepts optional sharedMotionRef, syncs motion to it
- useBlobbiCompanionState: Receives motionRef instead of motion object
- useBlobbiCompanionGaze: Uses refs for position/target values, stable RAF loop
2026-03-30 18:14:14 -03:00
Alex Gleason b8eb0a8549 Filter out malformed custom emoji reactions missing the emoji tag 2026-03-30 15:49:25 -05:00
filemon 5dac0214ea fix(blobbi): improve companion reaction animations and remove duplicate hooks
- Fix reaction logic in BlobbiCompanionVisual: now uses 'swaying' when walking
  instead of always returning 'idle' (previously dead code)
- Remove duplicate useBlobbiCompanion hooks from src/hooks/ and src/blobbi/core/hooks/
  (orphaned files, not imported anywhere)
- Verified lookMode='forward' does NOT block externalEyeOffset - eye tracking
  system correctly uses external offset when disableTracking is true
2026-03-30 16:57:01 -03:00
filemon 3ddb7c8ceb Merge branch 'main' into implement-reactions-clean 2026-03-30 16:35:29 -03:00
Alex Gleason 03d4b6c4f2 Include both 'e' and 'a' tags in deletion events for addressable events
NIP-09 deletion events for addressable events (kinds 30000-39999) now
include both an 'e' tag (event ID) and an 'a' tag (event coordinate)
to ensure deletion works on relays that only support one or the other.

- useDeleteEvent: accept optional pubkey/dTag params, auto-add 'a' tag
  for addressable kinds
- NoteMoreMenu: pass event pubkey and d-tag to useDeleteEvent
- BadgesPage: add missing 'e' tag to badge definition deletion
- useUserLists: add missing 'e' tag to list deletion
2026-03-30 13:47:05 -05:00
filemon 55b551f214 fix(blobbi): companion now updates reactively when stats change
ROOT CAUSE:
The companion layer was not updating live because:
1. It used a separate query ('companion-blobbi') that wasn't optimistically updated
2. It didn't use projected state, so it showed raw relay data without decay
3. Item use only invalidated queries without optimistic updates, causing relay latency

FIXES:
1. useBlobbiCompanionData now uses useBlobbisCollection (shared with BlobbiPage)
   - Shares the same query cache that gets optimistic updates
   - No longer has a separate stale query

2. useBlobbiCompanionData now applies projected state via useProjectedBlobbiState
   - Companion shows projected decay (recalculates every 60 seconds)
   - Same behavior as BlobbiPage

3. useBlobbiItemUse now optimistically updates the blobbi-collection cache
   - Uses setQueryData to immediately update the parsed companion
   - Companion visual updates instantly after actions
   - Also invalidates for background consistency check

DATA FLOW (after fix):
1. User performs action → useBlobbiItemUse publishes event
2. Optimistic update → setQueryData updates blobbi-collection cache
3. useBlobbisCollection returns new data → blobbi reference changes
4. useProjectedBlobbiState recalculates → projectedState changes
5. useBlobbiCompanionData creates new companion object with new stats
6. BlobbiCompanionLayer's companionStats memo recalculates
7. useStatusReaction sees new stats → resolves new recipe
8. Visual updates immediately

NO REMOUNT KEY NEEDED:
The fix works purely through React's normal reactivity:
- Object references change through the memo chain
- useStatusReaction's effect detects stat changes via reference comparison
- No forced remounts required
2026-03-30 15:38:53 -03:00
Alex Gleason 6fe17c1cfd Fix notifications collapsing multiple profile reactions from the same user
Profile reactions (kind 7 on kind 0) are intentionally allowed to be
multiple, unlike post reactions. Treat each profile reaction as a
standalone notification instead of grouping by referenced event ID,
which was causing only the latest reaction per user to be shown.
2026-03-30 13:19:00 -05:00
filemon 93ccb572e5 fix(blobbi): alternating spiral winding, body-aware food icon positioning
- Fix egg sick spiral winding: Inner 3 now uses clockwise=false for
  proper alternation across all 7 spirals (4 outer + 3 inner)
- Add body-aware food icon positioning for adults using detectBodyPath()
- Food icon now placed at upper-left relative to detected body bounds
- Update FoodIconConfig type to accept bodyPath for shape-aware placement
- Import detectBodyPath in recipe.ts for food icon positioning
2026-03-30 15:15:34 -03:00
Alex Gleason 03aa1e6dbc Fix oversized reaction emoji in comment context header 2026-03-30 13:03:25 -05:00
filemon 059fb67d26 fix(blobbi): layered egg spirals, natural adult dirt distribution, adult food icon
## Egg spiral layering:
- Added 7 spirals total (4 outer + 3 inner) for magical/dizzy effect
- Outer spirals: float around egg shell at varying distances
- Inner spirals: subtle spirals across the egg body itself
- Mixed colors: gray (#4b5563, #6b7280, #9ca3af) + white accents
- Varying sizes (0.45em to 1.1em), speeds (2s to 4s), directions
- All use true Archimedean spiral paths matching Blobbi dizzy eyes
- Counter-clockwise on left side, clockwise on right for visual balance

## Adult dirt distribution:
- Uses detected body bounds for natural placement
- Distributes across multiple zones (not clustered in center or edges):
  - Lower-left edge (primary)
  - Lower-right edge (primary)
  - Left-center lower area (secondary)
  - Right mid-lower contour (fill)
  - Left side contour (fill)
- Face region ends at 55% body height (all dirt below that)
- Mark length scales with body width (6% of width, min 3 units)

## Adult food icon position/size:
- Position: upper-left (x=55, y=45) instead of upper-right
- Size: 80% larger (scale=1.8) for better visibility
- Stroke width increased proportionally (1.5x)
- Higher opacity (0.75) vs baby (0.65)
- Baby unchanged: upper-right position, original size
2026-03-30 14:57:40 -03:00
filemon eec7f1d5b5 fix(blobbi): body-aware dirt placement, dizzy-style spirals, stronger front dust
## Adult dirt placement now uses real body silhouette:
- detectBodyPath() extracts full X/Y bounds from SVG path
- computeAdultDirtPositions() places marks relative to actual body
- Dirt at lower 35% of body height, near side edges
- Scales with body size (mark length = 5% of width)
- Fallback to conservative defaults if body not detected

## Egg spirals now match dizzy eye visual language:
- Uses same createSpiralPath() Archimedean spiral algorithm
- SVG-native animateTransform rotation (not CSS)
- Dark stroke color (#1f2937) matching dizzy eyes
- Positioned floating around egg, not inside shell
- Varying sizes and rotation speeds for visual interest

## Front/back dust distribution:
- Egg: 2 back particles below, 4 front particles at lower edges
- Baby: 3 back below body, 3 front at lower side edges
- Adult: 3 back below body, 3 front at lower edges (body-aware)
- Front dust: larger, higher opacity (0.75-0.8), darker color
- Back dust: smaller, lower opacity (0.55), lighter color
- All dust avoids face region, stays at lower body edges
2026-03-30 14:42:43 -03:00
filemon 5ab16fbbf3 fix(blobbi): reposition dirt marks to lower body edges, avoid facial zones
Protected zones (dirt marks NEVER appear here):
- Eyes, mouth, eyebrows
- Tears, saliva/drool, blush marks, sparkles
- Upper-center body area where face elements live

Preferred dirt placement zones:
- Lower-left edge of body silhouette
- Lower-right edge of body silhouette
- Bottom edge (well below face region)

Variant differences:
- Egg: dust at lower outer shell edges only, no center-front placement
- Baby (100x100): safe zone y > 72, prefer x < 35 or x > 65
- Adult (200x200): safe zone y > 120, prefer x < 85 or x > 115

Also updated dust particle positions to follow same rules.
2026-03-30 14:30:37 -03:00
filemon a74f7037ff fix(blobbi): variant-aware dirt marks, real spirals, and front-layer dust particles
- Add BlobbiVariant type to body effects system for coordinate scaling
- Separate dirt/stink positions for baby (100x100) vs adult (200x200) viewBox
- Pass variant through applyBodyEffects and applyVisualRecipe pipeline
- Replace egg sick curved paths with real Archimedean spirals using createEggSpiralPath()
- Add generateDustParticles() with front+back layer particles for stronger dirty read
- Increase dust particle opacity and use darker colors for visibility
- Add front-layer dirty particles to egg statusEffects
2026-03-30 14:14:32 -03:00
filemon 18cf251c7e feat(blobbi): add egg status effects and improve adult dirt placement
## Egg Form Visual Effects

### 1. Dirty State (new)
- Sweat droplet near upper-left of egg (blue gradient, slides down animation)
- Dust particles underneath the egg (gentle float-up animation)
- Triggered when recipe has dirtMarks or stinkClouds bodyEffects

### 2. Health/Sick State (new)
- Floating purple dizzy spirals around the egg (3 spirals, rotate animation)
- Replaces adult dizzy eyes since eggs don't have faces
- Triggered when recipe has dizzySpirals in eyes

### 3. Happy State (new)
- Golden sparkle stars around the egg (3 sparkles, twinkle animation)
- Simple 4-point or 8-point star shapes
- Triggered when reaction='happy' and no tears

### Implementation
- Added EggStatusEffects interface: { dirty, sick, happy }
- Props flow: BlobbiStageVisual → BlobbiEggVisual → EggGraphic
- Status effects derived from recipe in BlobbiStageVisual
- All animations respect prefers-reduced-motion

## Adult Form Dirt Placement

### Problem
Dirt marks were appearing outside the Blobbi body silhouette.

### Solution
- Repositioned dirt marks to be centered within body area
- X range: 42-56 (was 35-55) - more centered
- Y range: 55-78 (was 72-80) - better vertical spread
- Added positions for count=4-5 for severity escalation
- Reduced stroke width (1.3 vs 1.5) and opacity (0.55 vs 0.6) for subtlety
- Stink clouds also recentered (x: 44-56 vs 38-62)

New files/exports:
- EggStatusEffects type exported from @/blobbi/egg
- 4 new CSS animations: egg-sweat-drop, egg-dust-particle, egg-spiral, egg-sparkle
2026-03-30 13:46:31 -03:00
filemon 5de5488b24 fix(blobbi): critical health mouth overrides sleepy mouth priority
When health is critical, the dizzy round mouth now wins over sleepy mouth
regardless of energy being low. This ensures severe states read as
'urgent/sick/disoriented' rather than just 'tired'.

## New mouth precedence rule
Critical health bypasses the normal MOUTH_PRIORITY list entirely.
The check happens before pickPart() for mouth resolution.

## Scenarios where critical-health mouth now wins
- health critical + energy low → dizzy mouth (was: sleepy mouth)
- health critical + energy low + hunger low → dizzy mouth
- health critical + everything low → dizzy mouth
- any scenario with health=critical → dizzy mouth guaranteed

## Scenarios where sleepy mouth still wins
- energy low + health normal/warning/high → sleepy mouth
- energy low + any other stats (no critical health) → sleepy mouth
- ordinary tiredness without severe illness → sleepy mouth

The exception is minimal: one conditional check before normal priority
resolution, documented in both MOUTH_PRIORITY and inline comments.
2026-03-30 13:36:33 -03:00
filemon 83887b0516 docs(blobbi): cleanup pass for expression system consistency
## A) Fixed outdated/contradictory comments
- ENERGY_PARTS: Clarified that lower cycleDuration = heavier eyelids (not 'slower')
- MOUTH_PRIORITY: Updated doc to reflect hunger's severity progression (round→droopy)
- resolveStatusRecipe(): Updated example compositions to match current behavior
- recipe.ts module doc: Clarified two pathways (presets vs status-driven)

## B) Aligned EMOTION_RECIPES presets with status-driven behavior
- hungry preset: Updated to match 'high' severity (mouth 3.5x4.5, brows -14°)
- dirty preset: Updated to match 'high' severity (grimace 0.8/0.2, brows +10°)
- Added documentation explaining presets align with high/critical severity

## C) Drool semantics decision: kept hunger-driven
- Drool remains semantically tied to hunger (salivating for food)
- No other stat has natural reason to produce drool
- Architecture already supports it; no changes needed
- Added clarifying comment documenting this decision

## D) Validated multi-stat combinations
All tested combinations produce natural pet-like expressions:
- Single stats: Each has distinct, readable expression
- Multi-stats: Priority rules produce sensible compositions
- Extras additive: Multiple stats can contribute drool + tears
- Body effects: Dirt/stink shows regardless of facial expression

No priority changes needed — current rules work well.
2026-03-30 13:32:03 -03:00
Alex Gleason ec24c4cfae Add zap option to profile 3-dots menu
Move zap functionality into the ProfileMoreMenu so users with a
lightning address can still be zapped. The menu row opens the ZapDialog
after the more menu closes via a hidden trigger ref.
2026-03-30 11:25:03 -05:00
filemon 002461e7cb refactor(blobbi): refine expression quality, severity behavior, and drool positioning
## Hunger Progression
- warning: hopeful/asking (small round 'ooh' mouth, mild pleading brows)
- high: needy (bigger round mouth, more worried brows)
- critical: weak/desperate (droopy pleading mouth, very worried brows)

Hunger now feels like genuine plea progression from 'ooh, food?' to 'please...'
to 'I'm so hungry...' rather than same expression at all levels.

## Health Eye Priority
- Only CRITICAL health claims eyes (dizzy spirals)
- Warning/high health no longer override sadness/hunger eyes
- This lets sad/hungry eyes show through when health is merely warning/high

## Severity Escalation (all stats)
Each stat now has documented severity escalation:
- energy: sleepy → heavier sleepy → very drowsy (slower blink cycles)
- hunger: asking → needy → desperate (mouth shape progression)
- happiness: down → sad → crying (eye wetness + tears progression)
- hygiene: uncomfortable → gross → very gross (grimace + dirt escalation)
- health: weak → sick → dizzy (only critical gets dizzy spirals)

## Drool Positioning Fix
- Added computeDroolAnchor() to calculate drool position based on final mouth shape
- Added generateDroolAtAnchor() to render drool at computed anchor
- Drool now correctly attaches to roundMouth, droopyMouth, sadMouth edges
- Previously drool used original mouth position, looked detached with some shapes

## Priority Order (unchanged)
Eyes: health(critical) > energy > happiness > hunger > hygiene
Mouth: energy > health > happiness > hunger > hygiene
Eyebrows: health > hunger > happiness > hygiene > energy
2026-03-30 13:21:47 -03:00
Alex Gleason d12e75ae5c Replace zap button with emoji reaction button on user profiles
Add ProfileReactionButton component that opens an emoji picker to send
kind 7 reactions to a user's profile with a, e, and p tags. Update
notifications to display 'reacted to your profile' for profile reactions
and skip rendering the referenced card for kind 0 events.
2026-03-30 11:06:32 -05:00
filemon a480379fa5 refactor(blobbi): refine emotion presets and part contributions for natural pet-like expressions
- Update EMOTION_RECIPES with better documentation and design principles
- Add design comments explaining each preset's purpose and feeling
- Refine hungry preset: pleading/hopeful (not sad) with round anticipating mouth
- Refine dirty preset: uncomfortable/irritated grimace with furrowed brows
- Refine dizzy preset: add distressed raised brows for urgency
- Update ENERGY_PARTS: emphasize relaxed drowsiness, no forced brows
- Update HEALTH_PARTS: weak/unwell feeling distinct from sadness
- Update HUNGER_PARTS: pleading expression with open 'ooh food?' mouth
- Update HYGIENE_PARTS: irritated grimace, slightly furrowed brows
- Update HAPPINESS_PARTS: genuine emotional sadness with progressive tears

Each stat now has a distinct visual 'personality' that creates empathy:
- Hunger evokes nurturing (hopeful, pleading)
- Energy evokes tiredness (drowsy, fading)
- Health evokes concern (weak, unwell)
- Hygiene evokes discomfort (irritated, 'I feel gross')
- Happiness evokes emotional connection (genuine sadness)
2026-03-30 12:42:47 -03:00
filemon c37d0d15a6 feat(blobbi): part-priority status reaction system for natural expressions
Replace the old 'single winning preset' approach in resolveStatusRecipe()
with a part-priority composition system. Each low stat now contributes
independently to eyes, mouth, eyebrows, extras, and bodyEffects.

Architecture:
- Each stat has a PartContributionResolver that returns what it contributes
  to each facial/body part at each severity level (warning/high/critical).
- Exclusive parts (eyes, mouth, eyebrows) use per-part priority lists to
  decide which stat wins that slot when multiple stats are low.
- Additive parts (extras, bodyEffects) merge contributions from all low
  stats simultaneously.

Part priority rules:
- Eyes: health(critical/dizzy) > energy(sleepy) > happiness(watery) > hunger
- Mouth: energy(sleepy) > health(sad/round) > happiness(sad) > hunger(droopy)
- Eyebrows: health > hunger(worried) > happiness(lowered) > hygiene(flat)
- Extras: additive — drool+food(hunger), tears(happiness), all coexist
- BodyEffects: additive — dirt+stink(hygiene), anger-rise, all coexist

Severity escalation examples:
- Happiness tears only appear at high/critical, not warning
- Health switches from sad face to dizzy spirals at critical
- Hunger droopiness and dirt mark counts scale with severity
- Happiness eye water fill only at critical (full crying)

Combined stat examples:
- hunger + hygiene: hungry eyes, droopy mouth, worried brows, drool +
  food icon, dirt + stink clouds
- energy + hunger: sleepy eyes, sleepy mouth, hungry eyebrows, drool +
  food icon
- health(critical) + energy: dizzy eyes (beats sleepy), sleepy mouth,
  health eyebrows
- all stats low: prioritized eyes/mouth/brows, additive drool + tears

Also adds 'sick' and 'dirty' entries to LABEL_CYCLE_DURATIONS in the
hook to match the new stat-based label format.
2026-03-30 12:33:19 -03:00
filemon 79ccfd661a refactor(blobbi): final recipe-first consistency pass
- Rename emotionName → recipeLabel in applyVisualRecipe() signature and
  update SVG class names from blobbi-emotion to blobbi-recipe, since the
  parameter carries a recipe label (e.g. 'hungry-sleepy'), not strictly
  an emotion name.

- Guard against bodyEffects double-application in BlobbiAdultVisual and
  BlobbiBabyVisual: skip the manual applyBodyEffects() call when a
  recipeProp is provided, since applyVisualRecipe() already applies
  recipe.bodyEffects internally. The manual bodyEffects prop remains
  available for non-recipe use cases only.

- Update module-level docs in recipe.ts, status-reactions.ts, and
  useStatusReaction.ts to consistently describe the recipe-first
  architecture without leftover emotion-layer terminology.

- Remove unused BodyEffectConfig import from recipe.ts (pre-existing
  eslint error).
2026-03-30 12:14:40 -03:00
Alex Gleason 94f0c8308d Show all sidebar items to logged-out users and update sidebar order
Stop filtering requiresAuth items from navigation. Pages already render
their own LoginArea when the user is not logged in, so hiding the items
from the sidebar, mobile drawer, and search prevented feature discovery
without providing any benefit.

Also update the default sidebar order: remove bookmarks and profile,
add letters.
2026-03-30 09:56:02 -05:00
filemon c77b68eed2 fix(blobbi): eliminate body effects duplication, fix stat recovery, improve merged label timing
- Remove bodyEffects from StatusRecipeResult and useStatusReaction output.
  Body effects are folded into recipe.bodyEffects by resolveStatusRecipe()
  and applied once by applyVisualRecipe(). No separate channel needed.

- Fix stat recovery logic: re-resolve via resolveStatusRecipe() on every
  stat change instead of forcing neutral when previous triggering stat
  recovers. If energy recovers but hunger is still low, the hook now
  correctly transitions to the hungry recipe instead of neutral.

- Fix getRecipeCycleDuration() for merged labels (e.g. 'boring-sleepy'):
  compute Math.max() of all matching durations instead of returning the
  first match.

- Update all consumers (BlobbiPage, BlobbiCompanionLayer) to stop
  destructuring/passing bodyEffects from status reaction output.

- Update doc comments across visual components to clarify that the
  bodyEffects prop is for manual/external use only, not for status
  reaction data.
2026-03-30 11:52:00 -03:00
Chad Curtis 80820ae9c4 Expand compose textarea smoothly as you type 2026-03-30 07:36:16 -05:00
Chad Curtis 5288b7a718 Letters: overflow menu, reply button, grid layout, and UX polish
- EnvelopeCard: add ... overflow menu trigger using NoteMoreMenu (no duplicated logic)
- NoteMoreMenu: detect NIP-44 ciphertext by content shape, show 'Encrypted content'
- LetterDetailSheet: reply button (InkPenIcon, primary colors); gift above card; letter centered in viewport; click outside closes
- LettersPage: reply pre-fills sender npub; default stationery falls back to parchment when no custom theme; 2-col mobile / 4-col desktop grid
- NotificationsPage: reply button next to 'View all letters' in letter notifications
- ComposeLetterSheet: auto-focus textarea on open; fix To field overflow on narrow screens
- InkPenIcon: new custom icon replacing PenLine on FAB and reply buttons
2026-03-30 06:21:50 -05:00
Chad Curtis 4643830512 Sync letter improvements from lief
- Make LetterContent.body optional; a letter requires a non-empty body
  or at least one sticker
- Replace colors:[]/flatMode with event-stripping: flat color moment =
  event field stripped from Stationery, removing the colors? field
- Remove edgeScale: stickers render at their stored scale value with no
  edge-proximity size reduction, matching lief and the NIP
- Fix sticker shrinking near card edges: add max-width:none to override
  Tailwind preflight max-width:100% on img/svg elements
2026-03-30 04:30:00 -05:00
Chad Curtis ef04de67c0 Improve notification rendering for badges and letters
- Add hideKindHeader prop to NoteCard, used by ReferencedNoteCard to
  suppress redundant action headers in repost/reaction/zap notifications
- Redesign badge award notification: show full BadgeContent showcase card
  with prominent rounded-pill Accept Badge button below
- Redesign letter notification: larger centered envelope (minimal mode
  hides name/timestamp), click opens LetterDetailSheet inline, View All
  Letters button below
2026-03-30 00:13:01 -05:00
Chad Curtis 5847cceba6 Auto-shrink stickers near card edges and clip overflow at rounded boundary
Stickers now scale down proportionally as their center approaches any
edge of the letter card, preventing them from overflowing the rounded
corners. A 5% drag buffer keeps sticker centers away from the very
edge. The sticker overlay uses pointer-events-none so the textarea
remains clickable underneath.
2026-03-29 23:53:20 -05:00
Chad Curtis f62b86027c Remove unused eslint-disable directive in LayoutContext 2026-03-29 22:59:41 -05:00
Chad Curtis 8e5018d3b2 Fix 3 letter bugs: allow drawing-only sends, fix sticker drag bounds, preserve theme events
- Allow sending letters with only stickers/drawings (no body text required)
- Fix sticker drag positioning by using cardRef instead of page container
  ref, so percentage coordinates map correctly to the card boundaries
- Preserve stationery source events (color moments/themes) in preferences
  so they embed as gift attachments in sent letters
- Accept sticker-only letters during decryption validation
2026-03-29 22:41:03 -05:00
Chad Curtis 8df17f5ae7 Fix top bar arc flash by deferring layout cleanup past Suspense boundary 2026-03-29 22:23:23 -05:00
Alex Gleason dd31ce681f Add Blobbi to default sidebar order after Badges 2026-03-29 22:05:31 -05:00
filemon fd9a963b27 refactor(blobbi): complete recipe-first architecture, remove secondaryEmotion
Finish the migration from emotion-name composition to final visual
recipe resolution throughout the rendering pipeline.

Key changes:
- New emotion-types.ts: neutral type file for BlobbiEmotion/BlobbiVariant,
  breaking the import cycle between recipe.ts and emotions.ts
- status-reactions.ts: resolveStatusRecipe() now returns a fully resolved
  BlobbiVisualRecipe directly (merging sleepy+boring etc. internally)
- useStatusReaction: tracks resolved recipe state, outputs recipe+recipeLabel
  instead of emotion+secondaryEmotion
- Visual components (Adult, Baby, Stage): accept recipe+recipeLabel prop
  for recipe-first rendering; emotion prop kept as convenience for presets
- Companion components: pass recipe directly, no more secondaryEmotion
- BlobbiPage: passes resolved recipe from useStatusReaction to visuals
- emotions.ts: removed applyMergedEmotion() and mergeVisualRecipes re-export
- mergeVisualRecipes() stays in recipe.ts as an internal utility only used
  by status-reactions.ts for combining low-stat recipes

secondaryEmotion is fully eliminated from the codebase (0 occurrences).
The rendering path is now recipe-first end-to-end.
2026-03-29 23:37:27 -03:00
filemon 672d252492 refactor(blobbi): replace monolithic emotion system with part-based visual recipe architecture
Introduce BlobbiVisualRecipe as the central type for composing Blobbi
expressions from independent parts (eyes, mouth, eyebrows, bodyEffects,
extras). Named emotions are now presets that resolve into part-based
recipes via resolveVisualRecipe().

Key changes:
- New recipe.ts with BlobbiVisualRecipe types, EMOTION_RECIPES, and
  applyVisualRecipe() rendering pipeline
- emotions.ts becomes a thin public API delegating to recipe.ts
- Remove base/overlay emotion stacking model from status-reactions.ts
  and useStatusReaction.ts in favor of single emotion + secondaryEmotion
  for recipe-level merging
- Visual components (Adult, Baby, Stage) now resolve and merge recipes
  in a single pass instead of calling applyEmotion() twice
- Companion components updated to use secondaryEmotion prop
- All existing emotion presets preserved with identical visual output
- Backward-compatible: applyEmotion() API unchanged, legacy type aliases
  provided for EmotionConfig and EMOTION_CONFIGS
2026-03-29 23:16:25 -03:00
filemon bc4e00520e feat(blobbi): use stable idPrefix for body effect SVG element IDs
- applyEmotion now accepts optional instanceId parameter (5th arg)
- instanceId is passed through to applyBodyEffects as idPrefix
- BlobbiAdultVisual and BlobbiBabyVisual now pass blobbi.id as instanceId
- Anger-rise clip paths and gradients now use blobbi.id for stable IDs
  (e.g., blobbi-anger-clip-abc123 instead of random suffix)
- Random fallback still exists when instanceId is not provided
- Same Blobbi instance now produces deterministic SVG output
2026-03-29 22:03:00 -03:00
filemon d777d1bc98 refactor(blobbi): make bodyEffects/apply.ts the single entry point for body effects
- emotions.ts now delegates all body effects to applyBodyEffects()
- Removed direct imports of detectBodyPath, generateAngerRiseEffect,
  generateDirtMarks, generateStinkClouds from emotions.ts
- emotions.ts now only imports applyBodyEffects and BodyEffectsSpec
- Added unique ID generation for anger-rise clip paths and gradients
  (prevents collisions when multiple Blobbis render on same page)
- Body effects are applied after face overlays via single applyBodyEffects call
- Anger-rise overlay is still inserted right after body path for z-ordering
2026-03-29 21:53:30 -03:00
filemon 4cd97124da refactor(eyebrows): centralize class names and form offsets
- Add EYEBROW_CLASSES constant with all CSS class names
- Add FORM_EYEBROW_OFFSETS map for owli/froggi adjustments
- Rename keyframe from 'eyebrow-bounce' to 'blobbi-eyebrow-bounce'
- Export both constants from index.ts
- No behavior change, same public API
2026-03-29 21:08:42 -03:00
Alex Gleason 74345fdb2f Fix feed gaps when replies are disabled by over-fetching from relay
When followsFeedShowReplies is false, the relay limit was PAGE_SIZE (15)
but client-side reply filtering could discard most events, leaving only
a few visible posts per page with large time gaps between them.

Apply the same over-fetch pattern already used by useProfileFeed:
- Request PAGE_SIZE * 3 events when reply filtering is active
- Use rawCount (pre-filter) for pagination termination so pages where
  all items are replies don't prematurely stop pagination
2026-03-29 18:51:20 -05:00
filemon 7e7abdee3d simplify sleepy mouth to direct replacement
- Sleepy mouth is now clearly documented as a canonical standalone shape
- Direct replacement: no morph, transition, or interpolation between mouth states
- Keeps MouthAnchor architecture for stable positioning
- Updated docs across mouth/, types, and emotions.ts to be consistent
- Removed any wording suggesting transitions or morphing
2026-03-29 20:49:42 -03:00
filemon 9ed2127494 Introduce MouthAnchor for stable sleepy mouth positioning
applySleepyMouth no longer calls detectMouthPosition internally.
Instead, the orchestrator derives a MouthAnchor from the original
neutral SVG during the detection phase and passes it through.

This makes sleepy mouth placement reliable regardless of what base
emotion mouth (round ellipse, frown path, droopy, etc.) was applied
before the sleepy overlay runs.

mouth/types.ts:
- Added MouthAnchor interface ({ cx, cy })

mouth/detection.ts:
- Added mouthAnchorFromDetection(detection): derives { cx, cy } from
  a MouthDetectionResult (center of startX..endX, controlY)

mouth/generators.ts:
- applySleepyMouth now takes (svgText, anchor: MouthAnchor) instead of
  detecting internally. No more dependency on detectMouthPosition from
  within the sleepy mouth path. generateSleepyMouth is unchanged.

emotions.ts:
- Detection phase now computes mouthAnchor alongside mouth and eyes
- applySleepyAnimation receives the anchor and passes it through
- The anchor is always from the original unmodified SVG
2026-03-29 20:28:30 -03:00
filemon 30608ae8ed Merge branch 'main' into implement-reactions-clean 2026-03-29 20:24:04 -03:00
filemon ae43014cf2 Replace sleepy mouth morph with canonical breathing mouth shape
Remove the old sleepy mouth behavior that morphed the current mouth path
(smile → U-shape → smile via SMIL path animation). Replace with a
dedicated sleepy mouth: a small filled ellipse with a subtle breathing
animation (gentle expand/contract cycle, 3s period).

What changed:

mouth/generators.ts:
- Added generateSleepyMouth(centerX, centerY): produces a canonical
  small round ellipse (rx=2.8, ry=3.2) with SMIL breathing animation
- Added applySleepyMouth(svgText): detects current mouth position,
  generates the sleepy mouth, replaces whatever mouth is present
- Removed applySleepyMouthAnimation (the old morph-based approach)

mouth/detection.ts:
- Added replaceCurrentMouth(svgText, newMouthSvg): finds any element
  with blobbi-mouth class (path or ellipse, self-closing or with
  children) and replaces it. Falls back to Q-curve path matching.
  This handles all mouth types: base smile, sad frown, round mouth,
  droopy mouth, and previously-animated mouths.

mouth/types.ts:
- Removed SleepyMouthAnimationConfig (no longer needed)

emotions.ts:
- applySleepyAnimation no longer takes a mouth parameter
- Calls applySleepyMouth(svgText) instead of the old morph function
- Sleepy eye behavior (clip-path SMIL, closed-eye lines, wake-glance
  CSS, Zzz text) is completely unchanged

The sleepy mouth is now a proper canonical mouth shape in the mouth/
module, positioned at the detected mouth center, independent of
whatever base emotion mouth was applied before it.
2026-03-29 20:14:09 -03:00