Clean up first-hatch tour: real loading state, dev naming, reveal centering

- Wire real companionsFetching prop into BlobbiDashboard and pass it to
  useFirstHatchTourActivation instead of hardcoded isLoading: false
- Rename skipPostRequirement to skipToEggGlow in both BlobbiPage and
  BlobbiDevEditor (no post requirement exists in the simplified flow)
- Update dev button labels: 'Skip to Glow', 'Restart Tour',
  'Reset to Egg + Tour' with accurate titles/descriptions
- Handle async resetTour consistently with void in all onClick handlers
- Fix reveal overlay centering: move rays/glow/particles inside the
  Blobbi visual container (relative positioning) instead of a separate
  viewport-centered absolute layer, so the composition is anchored to
  the Blobbi regardless of header/naming section height
This commit is contained in:
filemon
2026-04-03 20:22:17 -03:00
parent f408f2424f
commit 60747de33c
3 changed files with 78 additions and 73 deletions
+13 -13
View File
@@ -29,9 +29,9 @@ import { ADULT_FORMS } from '@/blobbi/adult-blobbi/types/adult.types';
/** Tour dev actions for the first-hatch tour */
interface FirstHatchTourDevActions {
/** Skip the current step (for dev testing) */
skipPostRequirement: () => void;
/** Reset the tour in-memory state AND the profile tags so it can be tested again */
/** Jump from idle directly to egg_glowing_waiting_click (skip the auto-advance) */
skipToEggGlow: () => void;
/** Reset in-memory tour state AND Kind 11125 profile tags so the tour can re-activate */
resetTour: () => Promise<void>;
/** Current tour step id, or null if not active */
currentStepId: string | null;
@@ -561,25 +561,25 @@ export function BlobbiDevEditor({
</Badge>
</div>
<p className="text-xs text-muted-foreground">
Test the first-hatch tour flow without needing to create a real post.
Test the first-hatch tour. Profile tag <code>blobbi_first_hatch_tour_done</code> is the persisted signal.
</p>
<div className="flex flex-wrap gap-2">
{/* A. Skip Post Requirement */}
{/* A. Skip idle → egg glow */}
<Button
variant="outline"
size="sm"
onClick={() => {
tourDevActions.skipPostRequirement();
tourDevActions.skipToEggGlow();
}}
disabled={tourDevActions.currentStepId !== 'idle'}
className="gap-2 text-xs"
title="Skip to egg_glowing_waiting_click (skips idle)"
title="Jump from idle to egg_glowing_waiting_click"
>
<SkipForward className="size-3.5" />
Skip Post
Skip to Glow
</Button>
{/* B. Restart First-Hatch Tour (resets profile tags + in-memory state) */}
{/* B. Restart tour (resets profile tags + in-memory state) */}
<Button
variant="outline"
size="sm"
@@ -587,24 +587,24 @@ export function BlobbiDevEditor({
void tourDevActions.resetTour();
}}
className="gap-2 text-xs"
title="Reset first-hatch tour: clears profile tags and in-memory state"
title="Reset profile tags + in-memory state so the tour can re-activate"
>
<RefreshCw className="size-3.5" />
Restart Tour
</Button>
{/* C. Reset Blobbi to Egg */}
{/* C. Reset Blobbi to egg + restart tour */}
<Button
variant="outline"
size="sm"
onClick={() => {
setStage('egg');
setState('active');
tourDevActions.resetTour();
void tourDevActions.resetTour();
}}
disabled={companion.stage === 'egg'}
className="gap-2 text-xs"
title="Set stage to egg AND reset the tour — apply changes to test from scratch"
title="Set stage to egg + reset tour tags — click Apply to publish, then tour auto-starts"
>
<Egg className="size-3.5" />
Reset to Egg + Tour
@@ -123,61 +123,6 @@ export function BlobbiRevealOverlay({
{/* Backdrop */}
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" />
{/* Light rays behind the Blobbi */}
{!reducedMotion && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none overflow-hidden">
{/* Radial glow */}
<div
className={cn(
'absolute size-[500px] rounded-full',
'bg-gradient-radial from-amber-300/30 via-amber-200/10 to-transparent',
'transition-transform duration-1000 ease-out',
isVisible ? 'scale-100' : 'scale-50',
)}
style={{
background: 'radial-gradient(circle, rgba(251,191,36,0.3) 0%, rgba(251,191,36,0.1) 35%, transparent 70%)',
}}
/>
{/* Rotating rays */}
<div
className={cn(
'absolute size-[600px]',
isVisible ? 'animate-spin-slow' : '',
)}
style={{ animationDuration: '20s' }}
>
{[...Array(8)].map((_, i) => (
<div
key={i}
className="absolute top-1/2 left-1/2 h-[300px] w-[2px] -translate-x-1/2 origin-top"
style={{
transform: `translateX(-50%) rotate(${i * 45}deg)`,
background: 'linear-gradient(to bottom, rgba(251,191,36,0.4), transparent)',
}}
/>
))}
</div>
{/* Floating particles */}
{[...Array(12)].map((_, i) => (
<div
key={i}
className={cn(
'absolute size-1.5 rounded-full bg-amber-300/60',
'animate-float-particle',
)}
style={{
left: `${20 + Math.random() * 60}%`,
top: `${20 + Math.random() * 60}%`,
animationDelay: `${i * 0.3}s`,
animationDuration: `${2 + Math.random() * 2}s`,
}}
/>
))}
</div>
)}
{/* Center content */}
<div
ref={contentRef}
@@ -200,16 +145,73 @@ export function BlobbiRevealOverlay({
<Sparkles className="size-5" />
</div>
{/* Blobbi visual */}
{/* Blobbi visual — rays/glow are positioned relative to this container
so the composition is always centered on the Blobbi itself */}
<div className={cn(
'relative flex items-center justify-center',
'transition-all duration-700 delay-200',
isVisible ? 'translate-y-0 scale-100' : 'translate-y-4 scale-90',
)}>
{/* Light rays + glow (centered on the Blobbi) */}
{!reducedMotion && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none overflow-visible">
{/* Radial glow */}
<div
className={cn(
'absolute size-[500px] rounded-full',
'transition-transform duration-1000 ease-out',
isVisible ? 'scale-100' : 'scale-50',
)}
style={{
background: 'radial-gradient(circle, rgba(251,191,36,0.3) 0%, rgba(251,191,36,0.1) 35%, transparent 70%)',
}}
/>
{/* Rotating rays */}
<div
className={cn(
'absolute size-[600px]',
isVisible ? 'animate-spin-slow' : '',
)}
style={{ animationDuration: '20s' }}
>
{[...Array(8)].map((_, i) => (
<div
key={i}
className="absolute top-1/2 left-1/2 h-[300px] w-[2px] -translate-x-1/2 origin-top"
style={{
transform: `translateX(-50%) rotate(${i * 45}deg)`,
background: 'linear-gradient(to bottom, rgba(251,191,36,0.4), transparent)',
}}
/>
))}
</div>
{/* Floating particles */}
{[...Array(12)].map((_, i) => (
<div
key={i}
className={cn(
'absolute size-1.5 rounded-full bg-amber-300/60',
'animate-float-particle',
)}
style={{
left: `${20 + Math.random() * 60}%`,
top: `${20 + Math.random() * 60}%`,
animationDelay: `${i * 0.3}s`,
animationDuration: `${2 + Math.random() * 2}s`,
}}
/>
))}
</div>
)}
{/* The actual Blobbi */}
<BlobbiStageVisual
companion={companion}
size="lg"
animated
className="size-48 sm:size-56 drop-shadow-[0_0_30px_rgba(251,191,36,0.3)]"
className="relative z-10 size-48 sm:size-56 drop-shadow-[0_0_30px_rgba(251,191,36,0.3)]"
/>
</div>
+6 -3
View File
@@ -773,6 +773,7 @@ function BlobbiContent() {
invalidateCompanion={invalidateCompanion}
setStoredSelectedD={setStoredSelectedD}
ensureCanonicalBeforeAction={ensureCanonicalBeforeAction}
companionsFetching={companionFetching}
// DEV ONLY: State editor props
showDevEditor={showDevEditor}
setShowDevEditor={setShowDevEditor}
@@ -837,6 +838,8 @@ interface BlobbiDashboardProps {
profileAllTags: string[][];
profileStorage: import('@/blobbi/core/lib/blobbi').StorageItem[];
} | null>;
/** Whether companion data is still being fetched from relays */
companionsFetching: boolean;
// DEV ONLY: State editor props
showDevEditor: boolean;
setShowDevEditor: (show: boolean) => void;
@@ -870,6 +873,7 @@ function BlobbiDashboard({
invalidateCompanion,
setStoredSelectedD,
ensureCanonicalBeforeAction,
companionsFetching,
// DEV ONLY
showDevEditor,
setShowDevEditor,
@@ -993,7 +997,7 @@ function BlobbiDashboard({
const firstHatchTour = useFirstHatchTour();
useFirstHatchTourActivation({
companions,
isLoading: false,
isLoading: companionsFetching,
tour: firstHatchTour,
profileFirstHatchTourDone: profile?.firstHatchTourDone,
});
@@ -1177,8 +1181,7 @@ function BlobbiDashboard({
}, [firstHatchTour, profile, publishEvent, updateProfileEvent, invalidateProfile]);
const tourDevActions = useMemo(() => ({
skipPostRequirement: () => {
// No post requirement anymore - skip to glowing
skipToEggGlow: () => {
if (firstHatchTour.isStep('idle')) {
firstHatchTour.actions.goTo('egg_glowing_waiting_click');
}