ux(announcements): 🚸 Enhance AnnouncementModal usability with improved animations, accessibility features, and smoother dismissal flow
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
45114f89f4
commit
be5ec4e2bd
1 changed files with 53 additions and 2 deletions
|
|
@ -8,7 +8,7 @@
|
|||
* by the ResolvedAnnouncement passed as a prop.
|
||||
*/
|
||||
|
||||
import { useCallback, useState, useEffect, type ReactElement } from 'react';
|
||||
import { useCallback, useState, useEffect, useRef, type RefObject, type ReactElement } from 'react';
|
||||
|
||||
import { useSoundEngine } from '@lilith/ui-effects-sound';
|
||||
import { Modal } from '@lilith/ui-feedback';
|
||||
|
|
@ -28,6 +28,54 @@ const Title = motion.create(S.TitleBase);
|
|||
const SlideContainer = motion.create(S.SlideContainerBase);
|
||||
const BenefitItem = motion.create(S.BenefitItemBase);
|
||||
|
||||
// =============================================================================
|
||||
// Mouse Drift Hook
|
||||
// =============================================================================
|
||||
|
||||
const MAX_DRIFT_PX = 8;
|
||||
|
||||
function useMouseDrift(targetRef: RefObject<HTMLElement | null>): void {
|
||||
const rafId = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const el = targetRef.current;
|
||||
if (!el) return;
|
||||
|
||||
// Skip on touch-only devices
|
||||
const hasPointer = window.matchMedia('(pointer: fine)').matches;
|
||||
if (!hasPointer) return;
|
||||
|
||||
const onMouseMove = (e: MouseEvent): void => {
|
||||
cancelAnimationFrame(rafId.current);
|
||||
rafId.current = requestAnimationFrame(() => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
|
||||
// Normalize to viewport half-dimensions for consistent feel
|
||||
const halfW = window.innerWidth / 2;
|
||||
const halfH = window.innerHeight / 2;
|
||||
const rawX = (e.clientX - centerX) / halfW;
|
||||
const rawY = (e.clientY - centerY) / halfH;
|
||||
|
||||
// Damped clamp: proportional within ±1, capped at MAX_DRIFT_PX
|
||||
const x = Math.max(-MAX_DRIFT_PX, Math.min(MAX_DRIFT_PX, rawX * MAX_DRIFT_PX));
|
||||
const y = Math.max(-MAX_DRIFT_PX, Math.min(MAX_DRIFT_PX, rawY * MAX_DRIFT_PX));
|
||||
|
||||
el.style.transform = `translate(${x}px, ${y}px)`;
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
cancelAnimationFrame(rafId.current);
|
||||
el.style.transform = '';
|
||||
};
|
||||
}, [targetRef]);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Orientation Hook
|
||||
// =============================================================================
|
||||
|
|
@ -128,6 +176,9 @@ export const AnnouncementModal = ({
|
|||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const [direction, setDirection] = useState(0);
|
||||
const orientation = useOrientation();
|
||||
const iconWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useMouseDrift(iconWrapperRef);
|
||||
|
||||
const { definition, message, variantKey } = announcement;
|
||||
const { colorScheme, badge } = definition;
|
||||
|
|
@ -253,7 +304,7 @@ export const AnnouncementModal = ({
|
|||
animate={{ scale: 1, opacity: 1, rotate: 0 }}
|
||||
transition={{ duration: 0.5, ease: 'easeOut', type: 'spring' }}
|
||||
>
|
||||
<S.AnnouncementIconWrapper $accentColor={colorScheme.accent}>
|
||||
<S.AnnouncementIconWrapper ref={iconWrapperRef} $accentColor={colorScheme.accent}>
|
||||
<IconComponent size={48} />
|
||||
</S.AnnouncementIconWrapper>
|
||||
{BadgeIconComponent && (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue