The idleSpriteWrapperStyle was using `keyframes.toString()` inside a JS template literal, so Emotion's serializer hit the string-handling branch and never injected the @keyframes block — the animation silently did nothing. Switch to direct Keyframes-object interpolation inside css\`...\` so Emotion registers the rule and returns the animation name. Also add 'wheel' to ACTIVITY_EVENTS so desktop mouse-wheel scrolling on the inner scrollable content area resets the idle timer (the existing 'scroll' listener on window only catches mobile/touch scroll). Update the source plan doc to reflect the corrected idiom so future implementers don't repeat the bug. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
41 lines
1.0 KiB
TypeScript
41 lines
1.0 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
|
|
const IDLE_AFTER_MS = 30_000;
|
|
const TICK_MS = 2_000;
|
|
|
|
export type Mood = 'awake' | 'idle';
|
|
|
|
const ACTIVITY_EVENTS: Array<keyof WindowEventMap> = [
|
|
'pointerdown',
|
|
'touchstart',
|
|
'keydown',
|
|
'scroll',
|
|
'wheel',
|
|
];
|
|
|
|
export function useIdleMood(idleAfterMs: number = IDLE_AFTER_MS): Mood {
|
|
const [mood, setMood] = useState<Mood>('awake');
|
|
|
|
useEffect(() => {
|
|
let lastActivity = Date.now();
|
|
const onActivity = () => {
|
|
lastActivity = Date.now();
|
|
setMood((prev) => (prev === 'awake' ? prev : 'awake'));
|
|
};
|
|
ACTIVITY_EVENTS.forEach((evt) =>
|
|
window.addEventListener(evt, onActivity, { passive: true })
|
|
);
|
|
const tick = window.setInterval(() => {
|
|
if (Date.now() - lastActivity >= idleAfterMs) {
|
|
setMood((prev) => (prev === 'idle' ? prev : 'idle'));
|
|
}
|
|
}, TICK_MS);
|
|
return () => {
|
|
ACTIVITY_EVENTS.forEach((evt) => window.removeEventListener(evt, onActivity));
|
|
window.clearInterval(tick);
|
|
};
|
|
}, [idleAfterMs]);
|
|
|
|
return mood;
|
|
}
|