9ecd817bc6
- Inventory + currency + encumbrance vs carrying capacity - Class resources with correct short/long/daily rest recovery (applyRest) - Spellcasting: slots (+pact), known/prepared spells, derived DC + attack - Defenses: 5e death saves/inspiration/exhaustion, pf2e dying/wounded/hero points - Derived weapon attacks + passive perception via extended RulesSystem - Dexie v2 migration backfills new fields; debounced save flushes on pagehide - 9 new unit tests, new character-depth e2e; all green Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
53 lines
1.4 KiB
TypeScript
53 lines
1.4 KiB
TypeScript
import { useEffect, useMemo, useRef } from 'react';
|
|
|
|
/**
|
|
* Returns a debounced wrapper around `fn` that also flushes on unmount AND on
|
|
* page hide (tab close / refresh), so the last edit is never lost — the old app
|
|
* silently dropped unsaved character changes on navigation.
|
|
*/
|
|
export function useDebouncedCallback<A extends unknown[]>(
|
|
fn: (...args: A) => void,
|
|
delay = 400,
|
|
): (...args: A) => void {
|
|
const fnRef = useRef(fn);
|
|
fnRef.current = fn;
|
|
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const pending = useRef<A | null>(null);
|
|
|
|
const flush = () => {
|
|
if (timer.current) {
|
|
clearTimeout(timer.current);
|
|
timer.current = null;
|
|
}
|
|
if (pending.current) {
|
|
fnRef.current(...pending.current);
|
|
pending.current = null;
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const onHide = () => flush();
|
|
window.addEventListener('pagehide', onHide);
|
|
return () => {
|
|
window.removeEventListener('pagehide', onHide);
|
|
flush(); // flush on unmount
|
|
};
|
|
}, []);
|
|
|
|
return useMemo(
|
|
() =>
|
|
(...args: A) => {
|
|
pending.current = args;
|
|
if (timer.current) clearTimeout(timer.current);
|
|
timer.current = setTimeout(() => {
|
|
timer.current = null;
|
|
if (pending.current) {
|
|
fnRef.current(...pending.current);
|
|
pending.current = null;
|
|
}
|
|
}, delay);
|
|
},
|
|
[delay],
|
|
);
|
|
}
|