diff --git a/package.json b/package.json index b0a6f31..3dfc79f 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ }, "devDependencies": { "@eslint/js": "^9.30.1", + "@tanstack/react-store": "^0.7.5", + "@tanstack/store": "^0.7.5", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8258250..7fd797a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,12 @@ importers: '@eslint/js': specifier: ^9.30.1 version: 9.35.0 + '@tanstack/react-store': + specifier: ^0.7.5 + version: 0.7.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@tanstack/store': + specifier: ^0.7.5 + version: 0.7.5 '@types/react': specifier: ^19.1.8 version: 19.1.13 @@ -480,6 +486,15 @@ packages: cpu: [x64] os: [win32] + '@tanstack/react-store@0.7.5': + resolution: {integrity: sha512-A+WZtEnHZpvbKXm8qR+xndNKywBLez2KKKKEQc7w0Qs45GvY1LpRI3BTZNmELwEVim8+Apf99iEDH2J+MUIzlQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/store@0.7.5': + resolution: {integrity: sha512-qd/OjkjaFRKqKU4Yjipaen/EOB9MyEg6Wr9fW103RBPACf1ZcKhbhcu2S5mj5IgdPib6xFIgCUti/mKVkl+fRw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1054,6 +1069,11 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vite@7.1.6: resolution: {integrity: sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1453,6 +1473,15 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.50.2': optional: true + '@tanstack/react-store@0.7.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@tanstack/store': 0.7.5 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + use-sync-external-store: 1.5.0(react@19.1.1) + + '@tanstack/store@0.7.5': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.4 @@ -2067,6 +2096,10 @@ snapshots: dependencies: punycode: 2.3.1 + use-sync-external-store@1.5.0(react@19.1.1): + dependencies: + react: 19.1.1 + vite@7.1.6: dependencies: esbuild: 0.25.10 diff --git a/src/lib/skeleton.tsx b/src/lib/skeleton.tsx index 0cbd012..0939703 100644 --- a/src/lib/skeleton.tsx +++ b/src/lib/skeleton.tsx @@ -1,4 +1,5 @@ -import { createContext, useContext, useLayoutEffect, useState } from "react" +import { createContext, useContext, useLayoutEffect, useState, type PropsWithChildren } from "react" +import {Store} from "@tanstack/store"; const SkeletonContext = createContext({ componentCount: 0, @@ -6,15 +7,48 @@ const SkeletonContext = createContext({ removeComponent: () => { } }) -export function SkeletonProvider({ children }: { children: React.ReactNode }) { +const percentage = new Store(0); + +interface SkeletonProviderProps { + animationDuration?: number; // in milliseconds +} + +export function SkeletonProvider({ children, animationDuration: propsDuration }: PropsWithChildren) { const [componentCount, setComponentCount] = useState(0) + const animationDuration = propsDuration ?? 2000 + const addComponent = () => setComponentCount(count => count + 1) const removeComponent = () => setComponentCount(count => Math.max(0, count - 1)) - // Start a timer when the first component is added - // Stop the timer when the last component is removed - // Do not expose the timer to the context, instead make it a global singleton + useLayoutEffect(() => { + if (componentCount === 0) { + return + } + + // Count to 100 in `animationDuration` milliseconds, updating the percentage store + // Loop every `animationDuration` milliseconds so that the percentage goes from 0 to 100 repeatedly + let start: number | null = null + let frameId: number + + const step = (timestamp: number) => { + if (!start) start = timestamp + const elapsed = timestamp - start + const progress = Math.min(elapsed / animationDuration, 1) + percentage.setState(() => progress * 100) + if (elapsed < animationDuration) { + frameId = requestAnimationFrame(step) + } else { + start = null + frameId = requestAnimationFrame(step) + } + } + + frameId = requestAnimationFrame(step) + return () => { + cancelAnimationFrame(frameId) + } + }, [componentCount, animationDuration]) return (