Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | 2x 2x 42x 18x 18x 18x 18x 5x 15x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 2x 1x 12x 18x 6x 12x 12x 12x 12x 16x 1x 15x 20x 12x 12x 12x 12x 12x 12x 12x | /**
* The `fireConfetti()` one-liner — a module-level singleton that lazily
* creates (and reuses) one full-viewport overlay canvas, and removes it
* again once the engine has been idle for {@link SINGLETON_TEARDOWN_MS}.
*
* SSR-safe: on the server every call is a resolved no-op; nothing here
* touches `window`/`document` at module scope.
*
* Page visibility is handled engine-side with a plain `visibilitychange`
* listener (there is no React lifecycle to hook into here) — the loop
* pauses while the tab is hidden and resumes on return, with the engine's
* dt clamp preventing any catch-up burst.
*/
import { createConfettiEngine } from "./engine/createEngine";
import type { ConfettiEngine, FireOptions } from "./types";
/**
* How long the singleton stays idle before its canvas is removed from the
* DOM (a quick follow-up `fireConfetti()` reuses the warm canvas/engine).
*
* @example
* ```ts
* import { fireConfetti, SINGLETON_TEARDOWN_MS } from "@usefy/confetti";
*
* await fireConfetti();
* // the overlay canvas disappears SINGLETON_TEARDOWN_MS after going idle
* ```
*/
export const SINGLETON_TEARDOWN_MS = 3000;
interface SingletonState {
canvas: HTMLCanvasElement;
engine: ConfettiEngine;
teardownTimer: ReturnType<typeof setTimeout> | null;
unsubscribe: () => void;
onVisibilityChange: () => void;
}
let singleton: SingletonState | null = null;
function cancelTeardown(state: SingletonState): void {
if (state.teardownTimer !== null) {
clearTimeout(state.teardownTimer);
state.teardownTimer = null;
}
}
function scheduleTeardown(state: SingletonState): void {
cancelTeardown(state);
state.teardownTimer = setTimeout(() => {
resetConfetti();
}, SINGLETON_TEARDOWN_MS);
}
function ensureSingleton(): SingletonState {
if (singleton) return singleton;
const canvas = document.createElement("canvas");
canvas.setAttribute("aria-hidden", "true");
canvas.setAttribute("data-usefy-confetti", "singleton");
canvas.style.position = "fixed";
canvas.style.top = "0";
canvas.style.left = "0";
canvas.style.width = "100%";
canvas.style.height = "100%";
canvas.style.pointerEvents = "none";
canvas.style.zIndex = "1100";
document.body.appendChild(canvas);
const engine = createConfettiEngine(canvas);
const state: SingletonState = {
canvas,
engine,
teardownTimer: null,
unsubscribe: () => {},
onVisibilityChange: () => {
if (document.visibilityState === "hidden") engine.pause();
else engine.resume();
},
};
state.unsubscribe = engine.onActiveChange((active) => {
if (active) cancelTeardown(state);
else scheduleTeardown(state);
});
document.addEventListener("visibilitychange", state.onVisibilityChange);
// Armed from birth: if the first fire no-ops (reduced motion) the engine
// never flips active, and this timer still reclaims the canvas.
scheduleTeardown(state);
singleton = state;
return state;
}
/**
* Fire a confetti burst on an auto-managed full-viewport canvas — the
* 5-second quick start. The canvas is created lazily on first call, reused
* across calls, and removed automatically after the confetti has settled
* (see {@link SINGLETON_TEARDOWN_MS}).
*
* Resolves when the burst's particles have all died. On the server (SSR)
* — or under `prefers-reduced-motion: reduce` — it resolves immediately as
* a no-op.
*
* @example
* ```tsx
* import { fireConfetti } from "@usefy/confetti";
*
* <button onClick={() => fireConfetti({ origin: { y: 0.8 }, spread: 70 })}>
* 🎉 Ship it
* </button>
* ```
*/
export function fireConfetti(opts?: FireOptions): Promise<void> {
if (typeof window === "undefined" || typeof document === "undefined") {
return Promise.resolve(); // SSR no-op
}
return ensureSingleton().engine.fire(opts);
}
/**
* Tear the singleton down **now**: destroy the engine, remove the canvas
* and all listeners. Safe to call any time (no-op when nothing exists).
* The next {@link fireConfetti} starts fresh.
*
* @example
* ```ts
* import { resetConfetti } from "@usefy/confetti";
*
* // e.g. in a test's afterEach, or before an SPA full-page transition:
* resetConfetti();
* ```
*/
export function resetConfetti(): void {
if (!singleton) return;
const state = singleton;
singleton = null; // re-entrancy guard first
cancelTeardown(state);
state.unsubscribe(); // before destroy — its idle edge must not re-arm
document.removeEventListener("visibilitychange", state.onVisibilityChange);
state.engine.destroy();
state.canvas.parentNode?.removeChild(state.canvas);
}
|