@@ -37,8 +37,17 @@ interface Toast {
3737const toasts = ref <Toast []>([]);
3838let toastId = 0 ;
3939
40+ /** Hero badge — shown centered as a big celebration */
41+ const heroBadge = ref <Toast | null >(null );
42+
4043function addToast(toast : Omit <Toast , ' id' | ' leaving' >) {
4144 const t: Toast = { ... toast , id: ++ toastId , leaving: false };
45+ // Badge unlocks get hero treatment
46+ if (toast .type === ' badge' ) {
47+ heroBadge .value = t ;
48+ setTimeout (() => dismissHero (), 8000 );
49+ return ;
50+ }
4251 toasts .value .push (t );
4352 // Auto-dismiss after 5s
4453 setTimeout (() => dismiss (t .id ), 5000 );
@@ -54,6 +63,15 @@ function dismiss(id: number) {
5463 }
5564}
5665
66+ function dismissHero() {
67+ heroBadge .value = null ;
68+ }
69+
70+ function onHeroClick() {
71+ dismissHero ();
72+ document .dispatchEvent (new CustomEvent (' nge:open-profile' ));
73+ }
74+
5775// ── Track previous values to detect threshold crossings ──────────────────────
5876let prevEdits = 0 ;
5977let prevCells = 0 ;
@@ -217,6 +235,24 @@ watch(() => queueStore.proofread.size, () => {
217235<template >
218236 <ConfettiCelebration ref =" confettiRef" />
219237 <Teleport to =" body" >
238+ <!-- Hero badge unlock overlay -->
239+ <Transition name =" nge-hero" >
240+ <div v-if =" heroBadge" class =" nge-hero-overlay" @click =" onHeroClick" >
241+ <div class =" nge-hero-card" >
242+ <div class =" nge-hero-particles" >
243+ <span v-for =" i in 12" :key =" i" class =" nge-hero-particle" :style =" { '--i': i }" ></span >
244+ </div >
245+ <div class =" nge-hero-icon" >
246+ <img v-if =" heroBadge.isImage" :src =" heroBadge.icon" class =" nge-hero-badge-img" />
247+ <span v-else class =" nge-hero-emoji" >{{ heroBadge.icon }}</span >
248+ </div >
249+ <div class =" nge-hero-title" >{{ heroBadge.title }}</div >
250+ <div class =" nge-hero-subtitle" >{{ heroBadge.subtitle }}</div >
251+ <div class =" nge-hero-hint" >Click to view profile</div >
252+ </div >
253+ </div >
254+ </Transition >
255+
220256 <div class =" nge-toast-container" >
221257 <TransitionGroup name =" nge-toast" >
222258 <div
@@ -255,6 +291,78 @@ watch(() => queueStore.proofread.size, () => {
255291</template >
256292
257293<style scoped>
294+ /* ── Hero badge unlock (centered overlay) ── */
295+ .nge-hero-overlay {
296+ position : fixed ;
297+ inset : 0 ;
298+ z-index : 10000 ;
299+ display : flex ;
300+ align-items : center ;
301+ justify-content : center ;
302+ background : rgba (0 , 0 , 0 , 0.6 );
303+ backdrop-filter : blur (4px );
304+ cursor : pointer ;
305+ }
306+ .nge-hero-card {
307+ display : flex ;
308+ flex-direction : column ;
309+ align-items : center ;
310+ gap : 12px ;
311+ padding : 40px 56px ;
312+ background : rgba (18 , 22 , 30 , 0.97 );
313+ border : 1px solid rgba (255 , 200 , 80 , 0.3 );
314+ border-radius : 16px ;
315+ box-shadow :
316+ 0 0 80px rgba (245 , 166 , 35 , 0.15 ),
317+ 0 0 200px rgba (206 , 147 , 216 , 0.08 ),
318+ 0 24px 64px rgba (0 , 0 , 0 , 0.6 );
319+ position : relative ;
320+ overflow : hidden ;
321+ }
322+ .nge-hero-icon {
323+ width : 140px ; height : 140px ;
324+ display : flex ; align-items : center ; justify-content : center ;
325+ filter : drop-shadow (0 0 24px rgba (255 , 200 , 80 , 0.5 ));
326+ animation : nge-hero-float 3s ease-in-out infinite alternate ;
327+ }
328+ .nge-hero-badge-img { width : 120px ; height : 120px ; object-fit : contain ; }
329+ .nge-hero-emoji { font-size : 72px ; }
330+ .nge-hero-title {
331+ font-size : 20px ; font-weight : 700 ; color : #ffd08a ;
332+ text-align : center ; text-shadow : 0 0 20px rgba (245 , 166 , 35 , 0.4 );
333+ }
334+ .nge-hero-subtitle {
335+ font-size : 13px ; color : #999 ; text-align : center ; max-width : 280px ;
336+ }
337+ .nge-hero-hint {
338+ font-size : 11px ; color : rgba (255 , 208 , 138 , 0.5 ); margin-top : 8px ;
339+ }
340+ @keyframes nge-hero-float {
341+ from { transform : translateY (0 ); }
342+ to { transform : translateY (-8px ); }
343+ }
344+ /* Hero particles */
345+ .nge-hero-particles { position : absolute ; inset : 0 ; pointer-events : none ; overflow : hidden ; }
346+ .nge-hero-particle {
347+ position : absolute ; width : 4px ; height : 4px ; border-radius : 50% ;
348+ background : rgba (255 , 200 , 80 , 0.6 );
349+ animation : nge-hero-sparkle 2s ease-out infinite ;
350+ animation-delay : calc (var (--i ) * 0.15s );
351+ left : calc (5% + var (--i ) * 7.5% );
352+ top : 80% ;
353+ }
354+ @keyframes nge-hero-sparkle {
355+ 0% { opacity : 1 ; transform : translateY (0 ) scale (1 ); }
356+ 100% { opacity : 0 ; transform : translateY (-120px ) scale (0 ); }
357+ }
358+ /* Hero transitions */
359+ .nge-hero-enter-active { transition : all 0.4s cubic-bezier (0.34 , 1.56 , 0.64 , 1 ); }
360+ .nge-hero-leave-active { transition : all 0.3s ease-in ; }
361+ .nge-hero-enter-from { opacity : 0 ; }
362+ .nge-hero-enter-from .nge-hero-card { transform : scale (0.7 ); }
363+ .nge-hero-leave-to { opacity : 0 ; }
364+ .nge-hero-leave-to .nge-hero-card { transform : scale (0.9 ); }
365+
258366.nge-toast-container {
259367 position : fixed ;
260368 top : 52px ;
0 commit comments