Files
Archetype-FirstSpark/src/lib/useIdleMood.ts
gahusb 744ccbf434 fix: register idleYawn keyframes via direct css interpolation + cover wheel scroll (F-4)
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>
2026-05-04 04:01:47 +09:00

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;
}