← Public packages
@kentcdodds/package-app-kit
Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.
starter-remix/app/ui/busy.ts
72 lines · 2.0 KB · TypeScript/**
* Busy / pending helpers inspired by `spin-delay`:
* - Give immediate affordance on press (so taps never feel ignored)
* - Delay showing a "loading" label/spinner so fast ops do not flash
* - Once shown, keep it visible for a minimum duration
*/
export type BusyPhase = 'idle' | 'pending' | 'busy'
export type BusyGateOptions = {
/** Wait this long before flipping pending → busy (default 200ms). */
delayMs?: number
/** Once busy, stay busy at least this long (default 400ms). */
minDurationMs?: number
}
export function createBusyGate(options: BusyGateOptions = {}) {
const delayMs = options.delayMs ?? 200
const minDurationMs = options.minDurationMs ?? 400
let phase: BusyPhase = 'idle'
let delayTimer: ReturnType<typeof setTimeout> | null = null
let busySince = 0
let generation = 0
function clearDelay() {
if (delayTimer) {
clearTimeout(delayTimer)
delayTimer = null
}
}
return {
get phase() {
return phase
},
/** Call synchronously on pointer/click — marks pending immediately. */
start(onChange?: (phase: BusyPhase) => void) {
generation += 1
const gen = generation
clearDelay()
phase = 'pending'
onChange?.(phase)
delayTimer = setTimeout(() => {
if (gen !== generation) return
phase = 'busy'
busySince = Date.now()
onChange?.(phase)
}, delayMs)
},
/** Resolve when the async work finishes; respects min busy duration. */
async stop(onChange?: (phase: BusyPhase) => void) {
const gen = generation
clearDelay()
if (phase === 'busy') {
const elapsed = Date.now() - busySince
const wait = Math.max(0, minDurationMs - elapsed)
if (wait > 0) await new Promise((r) => setTimeout(r, wait))
}
if (gen !== generation) return
phase = 'idle'
onChange?.(phase)
},
/** True while the user should see a loading treatment (after delay). */
get showBusy() {
return phase === 'busy'
},
/** True from the moment of press until stop — use for disable / aria-busy. */
get isActive() {
return phase !== 'idle'
},
}
}