UNPKG

scrimr

Version:

🎲 A simple React shimmer component that displays animated random text while loading. Lightweight alternative to skeleton screens.

120 lines (116 loc) • 4.22 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { Scrimr: () => Scrimr, default: () => Scrimr_default }); module.exports = __toCommonJS(index_exports); // src/Scrimr.tsx var import_react = require("react"); // src/lib/utils.ts var import_clsx = require("clsx"); var import_tailwind_merge = require("tailwind-merge"); function cn(...inputs) { return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs)); } // src/Scrimr.tsx var import_jsx_runtime = require("react/jsx-runtime"); var DEFAULT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&* "; var clamp = (n, min, max) => Math.min(max, Math.max(min, n)); var pick = (chars) => chars[Math.floor(Math.random() * chars.length)]; function mutateText(text, chars, ratio) { if (!text) return text; const arr = text.split(""); const changes = Math.max(1, Math.floor(arr.length * ratio)); for (let i = 0; i < changes; i++) { const idx = Math.floor(Math.random() * arr.length); arr[idx] = pick(chars); } return arr.join(""); } var Scrimr = ({ isLoading, children, length = 20, speed = 30, chars = DEFAULT_CHARS, className, placeholderLabel = "Loading content", partialUpdateRatio = 0.8 }) => { const safeLength = clamp(Math.floor(length), 1, 5e3); const hasChars = typeof chars === "string" && chars.length > 0; const pool = hasChars ? chars : DEFAULT_CHARS; const [text, setText] = (0, import_react.useState)(""); const timerRef = (0, import_react.useRef)(void 0); const prefersReducedMotion = (0, import_react.useMemo)(() => { if (typeof window === "undefined" || !("matchMedia" in window)) return false; return window.matchMedia("(prefers-reduced-motion: reduce)").matches; }, []); (0, import_react.useEffect)(() => { if (!isLoading) { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = void 0; } return; } setText(Array.from({ length: safeLength }, () => pick(pool)).join("")); const interval = prefersReducedMotion ? 400 : speed; const tick = () => { if (typeof document !== "undefined" && document.visibilityState === "hidden") return; setText((prev) => { if (!prev) { return Array.from({ length: safeLength }, () => pick(pool)).join(""); } return mutateText(prev, pool, clamp(partialUpdateRatio, 0.05, 1)); }); }; timerRef.current = setInterval(tick, interval); return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = void 0; } }; }, [isLoading, safeLength, pool, speed, partialUpdateRatio, prefersReducedMotion]); if (!isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children }); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "span", { className: cn( "block animate-pulse text-gray-400 select-none font-mono truncate", className ), role: "status", "aria-live": "polite", "aria-label": placeholderLabel, "aria-busy": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: text }) } ); }; var Scrimr_default = Scrimr; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Scrimr }); //# sourceMappingURL=index.js.map