fix: moving focus as episodes load, loading indicator

This commit is contained in:
2026-09-22 10:04:19 -04:00
parent 649baf40ab
commit f5f4bc73b9
7 changed files with 517 additions and 3 deletions

46
src/hooks/useHeldFlag.ts Normal file
View File

@@ -0,0 +1,46 @@
/**
* useHeldFlag — keep a boolean true for a minimum time after it falls.
*
* A warm-cache fetch-more load begins and ends between two renderer
* frames: the raw `isLoadingMore` signal flips true→false without a
* single paint, so the "[Fetch More]" → spinner swap never appears and
* the press looks like a no-op. Components rendering a spinner for such
* bursts read through this hook instead of the raw signal — the spinner
* stays up (and animating) for at least `minMs` after the load ends,
* guaranteeing several painted frames.
*
* Deliberately a display-layer concern: the store's own `isLoadingMore`
* keeps its exact load-window semantics (guards, tests) and only the
* rendered indicators are held.
*/
import { createSignal, createEffect, onCleanup } from "solid-js";
export function useHeldFlag(
source: () => boolean,
minMs = 250,
): () => boolean {
const [held, setHeld] = createSignal(false);
let timer: ReturnType<typeof setTimeout> | null = null;
const clearTimer = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
createEffect(() => {
if (source()) {
// (Re)rising edge: show immediately; a pending fall from an
// earlier burst is cancelled.
clearTimer();
if (!held()) setHeld(true);
} else if (held()) {
// Falling edge: hold the flag up for the remaining window.
clearTimer();
timer = setTimeout(() => setHeld(false), minMs);
}
});
onCleanup(clearTimer);
return held;
}