├── .husky └── pre-commit ├── __mocks__ ├── styleMock.js └── fileMock.js ├── src ├── index.ts ├── components │ ├── flash │ │ ├── index.ts │ │ ├── flash.module.css │ │ └── flash.tsx │ ├── flip │ │ ├── index.ts │ │ ├── flip.tsx │ │ └── flip.module.css │ ├── glow │ │ ├── index.ts │ │ ├── glow.module.css │ │ └── glow.tsx │ ├── ping │ │ ├── index.ts │ │ ├── ping.module.css │ │ └── ping.tsx │ ├── pulse │ │ ├── index.ts │ │ ├── pulse.module.css │ │ └── pulse.tsx │ ├── shake │ │ ├── index.ts │ │ ├── shake.module.css │ │ └── shake.tsx │ ├── spin │ │ ├── index.ts │ │ ├── spin.module.css │ │ └── spin.tsx │ ├── swing │ │ ├── index.ts │ │ ├── swing.module.css │ │ └── swing.tsx │ ├── bounce │ │ ├── index.ts │ │ ├── bounce.module.css │ │ └── bounce.tsx │ ├── circle │ │ ├── index.ts │ │ ├── circle.tsx │ │ └── circle.module.css │ ├── squeeze │ │ ├── index.ts │ │ ├── squeeze.module.css │ │ └── squeeze.tsx │ ├── wiggle │ │ ├── index.ts │ │ ├── wiggle.tsx │ │ └── wiggle.module.css │ ├── animation.module.css │ ├── index.ts │ ├── utils.ts │ └── animation.tsx ├── globals.d.ts └── stories │ ├── common.module.css │ ├── swing │ ├── sign.module.css │ └── swing.stories.tsx │ ├── flip │ └── flip.stories.tsx │ ├── spin │ └── spin.stories.tsx │ ├── flash │ └── flash.stories.tsx │ ├── pulse │ └── pulse.stories.tsx │ ├── shake │ └── shake.stories.tsx │ ├── bounce │ └── bounce.stories.tsx │ ├── wiggle │ └── wiggle.stories.tsx │ ├── squeeze │ └── squeeze.stories.tsx │ ├── multi │ ├── pulse-wiggle │ │ └── pulse-wiggle.stories.tsx │ ├── pulse-circle │ │ └── pulse-squeeze.stories.tsx │ └── bounce-shake │ │ └── bounce-shake.stories.tsx │ ├── ping │ └── ping.stories.tsx │ ├── glow │ └── glow.stories.tsx │ └── circle │ └── circle.stories.tsx ├── jest.setup.ts ├── .eslintignore ├── .prettierignore ├── postcss.config.js ├── .npmignore ├── .prettierrc.json ├── .storybook ├── manager-head.html ├── main.ts ├── preview.css └── preview.tsx ├── .eslintrc.json ├── .gitignore ├── tsconfig.json ├── LICENSE ├── __tests__ └── components │ ├── flip.test.tsx │ ├── glow.test.tsx │ ├── ping.test.tsx │ ├── spin.test.tsx │ ├── flash.test.tsx │ ├── pulse.test.tsx │ ├── shake.test.tsx │ ├── swing.test.tsx │ ├── bounce.test.tsx │ ├── circle.test.tsx │ ├── wiggle.test.tsx │ └── squeeze.test.tsx ├── .github └── workflows │ ├── npmpublish.yml │ ├── storybook-deploy.yml │ └── chromatic.yml ├── README.md ├── rollup.config.mjs ├── package.json └── jest.config.ts /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm run precheck 2 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components' 2 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub' 2 | -------------------------------------------------------------------------------- /src/components/flash/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './flash' 2 | -------------------------------------------------------------------------------- /src/components/flip/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './flip' 2 | -------------------------------------------------------------------------------- /src/components/glow/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './glow' 2 | -------------------------------------------------------------------------------- /src/components/ping/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ping' 2 | -------------------------------------------------------------------------------- /src/components/pulse/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './pulse' 2 | -------------------------------------------------------------------------------- /src/components/shake/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './shake' 2 | -------------------------------------------------------------------------------- /src/components/spin/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './spin' 2 | -------------------------------------------------------------------------------- /src/components/swing/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './swing' 2 | -------------------------------------------------------------------------------- /src/components/bounce/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './bounce' 2 | -------------------------------------------------------------------------------- /src/components/circle/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './circle' 2 | -------------------------------------------------------------------------------- /src/components/squeeze/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './squeeze' 2 | -------------------------------------------------------------------------------- /src/components/wiggle/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './wiggle' 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .husky 3 | .pnpm-store 4 | pnpm-lock.yaml 5 | dist -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .husky 3 | .pnpm-store 4 | pnpm-lock.yaml 5 | dist -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css' 2 | declare module '*.module.scss' 3 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | webpack.local.config.js 2 | webpack.production.config.js 3 | .eslintrc.json 4 | .eslintignore 5 | .prettierrc.json 6 | .prettierignore 7 | .gitignore 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "semi": false, 6 | "tabWidth": 2, 7 | "jsxSingleQuote": true 8 | } 9 | -------------------------------------------------------------------------------- /.storybook/manager-head.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/flash/flash.module.css: -------------------------------------------------------------------------------- 1 | .animation { 2 | animation-name: flash; 3 | animation-timing-function: linear; 4 | } 5 | 6 | @keyframes flash { 7 | 20%, 8 | 80% { 9 | opacity: 1; 10 | } 11 | 50% { 12 | opacity: 0; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/pulse/pulse.module.css: -------------------------------------------------------------------------------- 1 | .animation { 2 | animation-name: pulse; 3 | animation-timing-function: linear; 4 | } 5 | 6 | @keyframes pulse { 7 | 20% { 8 | transform: scale(0.8); 9 | } 10 | 80% { 11 | transform: scale(1.2); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["prettier", "plugin:@typescript-eslint/recommended", "plugin:storybook/recommended"], 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "storybook"], 5 | "rules": { 6 | "@typescript-eslint/no-explicit-any": "off" 7 | }, 8 | "root": true 9 | } 10 | -------------------------------------------------------------------------------- /src/components/bounce/bounce.module.css: -------------------------------------------------------------------------------- 1 | .animation { 2 | animation-name: bounce; 3 | } 4 | 5 | @keyframes bounce { 6 | 20%, 7 | 50%, 8 | 80% { 9 | transform: translateY(0); 10 | } 11 | 40% { 12 | transform: translateY(-25%); 13 | } 14 | 60% { 15 | transform: translateY(-15%); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/glow/glow.module.css: -------------------------------------------------------------------------------- 1 | .animation { 2 | animation-name: glow; 3 | border-radius: inherit; 4 | } 5 | 6 | @keyframes glow { 7 | 20%, 8 | 80% { 9 | box-shadow: 0 0 var(--offset) calc(var(--offset) * -1) var(--color); 10 | } 11 | 50% { 12 | box-shadow: 0 0 var(--offset) var(--offset) var(--color); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/squeeze/squeeze.module.css: -------------------------------------------------------------------------------- 1 | .animation { 2 | animation-name: squeeze; 3 | } 4 | 5 | @keyframes squeeze { 6 | 20%, 7 | 80% { 8 | transform: scale(1, 1); 9 | } 10 | 30% { 11 | transform: scale(0.9, 1.1); 12 | } 13 | 50% { 14 | transform: scale(1.1, 0.9); 15 | } 16 | 70% { 17 | transform: scale(0.95, 1.05); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/animation.module.css: -------------------------------------------------------------------------------- 1 | .baseAnimation { 2 | animation-duration: var(--duration); 3 | animation-iteration-count: var(--iterations); 4 | animation-direction: var(--direction); 5 | } 6 | 7 | .noAnimation { 8 | animation: none; 9 | } 10 | 11 | .noPseudoAnimation { 12 | &::before { 13 | animation: none !important; 14 | display: none; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/spin/spin.module.css: -------------------------------------------------------------------------------- 1 | .animation { 2 | animation-name: spin; 3 | animation-timing-function: linear; 4 | } 5 | 6 | @keyframes spin { 7 | 0% { 8 | transform: rotate(0deg); 9 | } 10 | 20% { 11 | transform: rotate(1deg); 12 | } 13 | 80% { 14 | transform: rotate(359deg); 15 | } 16 | 100% { 17 | transform: rotate(360deg); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/shake/shake.module.css: -------------------------------------------------------------------------------- 1 | .animation { 2 | animation-name: shake; 3 | animation-timing-function: ease; 4 | } 5 | 6 | @keyframes shake { 7 | 20%, 8 | 80% { 9 | transform: translate3d(0, 0, 0); 10 | } 11 | 12 | 30%, 13 | 50%, 14 | 70% { 15 | transform: translate3d(-4px, 0, 0); 16 | } 17 | 18 | 40%, 19 | 60% { 20 | transform: translate3d(4px, 0, 0); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/swing/swing.module.css: -------------------------------------------------------------------------------- 1 | .animation { 2 | transform-origin: top center; 3 | animation-name: swing; 4 | } 5 | 6 | @keyframes swing { 7 | 10% { 8 | transform: rotate(0deg); 9 | } 10 | 30% { 11 | transform: rotate(15deg); 12 | } 13 | 50% { 14 | transform: rotate(-10deg); 15 | } 16 | 70% { 17 | transform: rotate(5deg); 18 | } 19 | 90% { 20 | transform: rotate(0deg); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/flip/flip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Animation from '../animation' 3 | import { BaseProps, defaultProps } from '../utils' 4 | import css from './flip.module.css' 5 | 6 | export interface Props extends BaseProps {} 7 | 8 | /** 9 | * A flip animation. 10 | */ 11 | const Flip = (props: Props) => 12 | 13 | Flip.defaultProps = defaultProps 14 | 15 | export default Flip 16 | -------------------------------------------------------------------------------- /src/components/spin/spin.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Animation from '../animation' 3 | import { BaseProps, defaultProps } from '../utils' 4 | import css from './spin.module.css' 5 | 6 | export interface Props extends BaseProps {} 7 | 8 | /** 9 | * A spin animation. 10 | */ 11 | const Spin = (props: Props) => 12 | 13 | Spin.defaultProps = defaultProps 14 | 15 | export default Spin 16 | -------------------------------------------------------------------------------- /src/components/flash/flash.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Animation from '../animation' 3 | import { BaseProps, defaultProps } from '../utils' 4 | import css from './flash.module.css' 5 | 6 | export interface Props extends BaseProps {} 7 | 8 | /** 9 | * A flash animation. 10 | */ 11 | const Flash = (props: Props) => 12 | 13 | Flash.defaultProps = defaultProps 14 | 15 | export default Flash 16 | -------------------------------------------------------------------------------- /src/components/pulse/pulse.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Animation from '../animation' 3 | import { BaseProps, defaultProps } from '../utils' 4 | import css from './pulse.module.css' 5 | 6 | export interface Props extends BaseProps {} 7 | 8 | /** 9 | * A pulse animation. 10 | */ 11 | const Pulse = (props: Props) => 12 | 13 | Pulse.defaultProps = defaultProps 14 | 15 | export default Pulse 16 | -------------------------------------------------------------------------------- /src/components/shake/shake.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Animation from '../animation' 3 | import { BaseProps, defaultProps } from '../utils' 4 | import css from './shake.module.css' 5 | 6 | export interface Props extends BaseProps {} 7 | 8 | /** 9 | * A shake animation. 10 | */ 11 | const Shake = (props: Props) => 12 | 13 | Shake.defaultProps = defaultProps 14 | 15 | export default Shake 16 | -------------------------------------------------------------------------------- /src/components/swing/swing.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Animation from '../animation' 3 | import { BaseProps, defaultProps } from '../utils' 4 | import css from './swing.module.css' 5 | 6 | export interface Props extends BaseProps {} 7 | 8 | /** 9 | * A swing animation. 10 | */ 11 | const Swing = (props: Props) => 12 | 13 | Swing.defaultProps = defaultProps 14 | 15 | export default Swing 16 | -------------------------------------------------------------------------------- /src/components/bounce/bounce.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Animation from '../animation' 3 | import { BaseProps, defaultProps } from '../utils' 4 | import css from './bounce.module.css' 5 | 6 | export interface Props extends BaseProps {} 7 | 8 | /** 9 | * A bounce animation. 10 | */ 11 | const Bounce = (props: Props) => 12 | 13 | Bounce.defaultProps = defaultProps 14 | 15 | export default Bounce 16 | -------------------------------------------------------------------------------- /src/components/wiggle/wiggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Animation from '../animation' 3 | import { BaseProps, defaultProps } from '../utils' 4 | import css from './wiggle.module.css' 5 | 6 | export interface Props extends BaseProps {} 7 | 8 | /** 9 | * A wiggle animation. 10 | */ 11 | const Wiggle = (props: Props) => 12 | 13 | Wiggle.defaultProps = defaultProps 14 | 15 | export default Wiggle 16 | -------------------------------------------------------------------------------- /src/components/squeeze/squeeze.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Animation from '../animation' 3 | import { BaseProps, defaultProps } from '../utils' 4 | import css from './squeeze.module.css' 5 | 6 | export interface Props extends BaseProps {} 7 | 8 | /** 9 | * A squeeze animation. 10 | */ 11 | const Squeeze = (props: Props) => 12 | 13 | Squeeze.defaultProps = defaultProps 14 | 15 | export default Squeeze 16 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Bounce } from './bounce' 2 | export { default as Circle } from './circle' 3 | export { default as Flash } from './flash' 4 | export { default as Flip } from './flip' 5 | export { default as Ping } from './ping' 6 | export { default as Pulse } from './pulse' 7 | export { default as Shake } from './shake' 8 | export { default as Spin } from './spin' 9 | export { default as Squeeze } from './squeeze' 10 | export { default as Swing } from './swing' 11 | export { default as Wiggle } from './wiggle' 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .pnpm-store 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | /dist 16 | 17 | # misc 18 | .DS_Store 19 | .env 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | .vscode 30 | Thumbs.db 31 | 32 | scripts 33 | 34 | dev 35 | 36 | .next 37 | .swc 38 | test-results 39 | *storybook.log 40 | storybook-static -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-webpack5' 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 5 | addons: [ 6 | '@storybook/addon-webpack5-compiler-swc', 7 | '@storybook/addon-links', 8 | '@storybook/addon-essentials', 9 | '@chromatic-com/storybook', 10 | '@storybook/addon-interactions', 11 | ], 12 | docs: { 13 | defaultName: 'Docs', 14 | }, 15 | framework: { 16 | name: '@storybook/react-webpack5', 17 | options: {}, 18 | }, 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /src/components/ping/ping.module.css: -------------------------------------------------------------------------------- 1 | .outer { 2 | position: relative; 3 | border-radius: inherit; 4 | } 5 | 6 | .animation { 7 | animation-name: ping; 8 | animation-timing-function: cubic-bezier(0, 0, 0.2, 1); 9 | position: absolute; 10 | border-radius: inherit; 11 | width: 100%; 12 | height: 100%; 13 | background-color: var(--color); 14 | z-index: -1; 15 | } 16 | 17 | @keyframes ping { 18 | 10% { 19 | transform: scale(1); 20 | opacity: 1; 21 | } 22 | 55%, 23 | 80% { 24 | transform: scale(var(--scale)); 25 | opacity: 0; 26 | } 27 | 100% { 28 | opacity: 0; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Default 4 | "target": "es5", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | 10 | // Added 11 | "lib": ["dom", "esnext"], 12 | "jsx": "react", 13 | "module": "ESNext", 14 | "declaration": true, 15 | "declarationDir": "types", 16 | "sourceMap": true, 17 | "outDir": "dist", 18 | "moduleResolution": "node", 19 | "allowSyntheticDefaultImports": true, 20 | "types": ["@testing-library/jest-dom"] 21 | }, 22 | "include": ["src/**/*"] 23 | } 24 | -------------------------------------------------------------------------------- /src/components/flip/flip.module.css: -------------------------------------------------------------------------------- 1 | .animation { 2 | backface-visibility: visible !important; 3 | animation-name: flip; 4 | animation-timing-function: ease; 5 | } 6 | 7 | @keyframes flip { 8 | 20% { 9 | transform: rotateY(0); 10 | animation-timing-function: linear; 11 | } 12 | 40% { 13 | transform: translateZ(150px) rotateY(170deg); 14 | animation-timing-function: ease-out; 15 | } 16 | 60% { 17 | transform: translateZ(150px) rotateY(190deg); 18 | animation-timing-function: ease-in; 19 | } 20 | 80% { 21 | transform: rotateY(360deg); 22 | animation-timing-function: linear; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/wiggle/wiggle.module.css: -------------------------------------------------------------------------------- 1 | .animation { 2 | animation-name: wiggle; 3 | animation-timing-function: ease; 4 | } 5 | 6 | @keyframes wiggle { 7 | 20% { 8 | transform: rotate(0deg); 9 | } 10 | 25% { 11 | transform: rotate(5deg); 12 | } 13 | 35% { 14 | transform: rotate(-5deg); 15 | } 16 | 40% { 17 | transform: rotate(0deg); 18 | } 19 | 45% { 20 | transform: rotate(5deg); 21 | } 22 | 55% { 23 | transform: rotate(-5deg); 24 | } 25 | 60% { 26 | transform: rotate(0deg); 27 | } 28 | 65% { 29 | transform: rotate(5deg); 30 | } 31 | 75% { 32 | transform: rotate(-5deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.storybook/preview.css: -------------------------------------------------------------------------------- 1 | .storybook-wrapper button { 2 | background-color: #f1f1f1; 3 | border: 1px solid #f1f1f1; 4 | border-radius: 5px; 5 | color: #333; 6 | cursor: pointer; 7 | font-size: 1rem; 8 | padding: 0.5rem 1rem; 9 | text-align: center; 10 | text-decoration: none; 11 | transition: 12 | background-color 0.3s, 13 | color 0.3s; 14 | } 15 | 16 | .storybook-wrapper input { 17 | background-color: #f1f1f1; 18 | border: 1px solid #dadada; 19 | border-radius: 5px; 20 | color: #333; 21 | font-size: 1rem; 22 | padding: 0.5rem 1rem; 23 | text-align: center; 24 | } 25 | 26 | .storybook-wrapper { 27 | display: inline-block; 28 | } 29 | -------------------------------------------------------------------------------- /src/stories/common.module.css: -------------------------------------------------------------------------------- 1 | .fancyBtn { 2 | background-color: #f1f1f1; 3 | border: 1px solid #f1f1f1; 4 | border-radius: 5px; 5 | color: #333; 6 | cursor: pointer; 7 | font-size: 1rem; 8 | padding: 0.5rem 1rem; 9 | text-align: center; 10 | text-decoration: none; 11 | transition: 12 | background-color 0.3s, 13 | color 0.3s; 14 | } 15 | 16 | .fancyDiv { 17 | background-color: #f1f1f1; 18 | border: 1px solid #f1f1f1; 19 | border-radius: 5px; 20 | width: 100px; 21 | height: 100px; 22 | text-align: center; 23 | } 24 | 25 | .fancyInput { 26 | background-color: #f1f1f1; 27 | border: 1px solid #dadada; 28 | border-radius: 5px; 29 | color: #333; 30 | font-size: 1rem; 31 | padding: 0.5rem 1rem; 32 | text-align: center; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/glow/glow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Animation from '../animation' 3 | import { BaseProps, defaultProps } from '../utils' 4 | import css from './glow.module.css' 5 | 6 | export interface Props extends BaseProps { 7 | /** The thickness of the line animation. */ 8 | thickness?: number 9 | /** The color of the line animation. */ 10 | color?: string 11 | } 12 | 13 | /** 14 | * A glow animation. 15 | */ 16 | const Glow = (props: Props) => { 17 | const style = { 18 | '--offset': `${props.thickness || 5}px`, 19 | '--color': `${props.color || 'white'}`, 20 | } as React.CSSProperties 21 | 22 | return 23 | } 24 | 25 | Glow.defaultProps = { 26 | ...defaultProps, 27 | thickness: 5, 28 | color: 'white', 29 | } 30 | 31 | export default Glow 32 | -------------------------------------------------------------------------------- /src/stories/swing/sign.module.css: -------------------------------------------------------------------------------- 1 | .arrowUp { 2 | position: absolute; 3 | width: 0; 4 | height: 0; 5 | border-left: 40px solid transparent; 6 | border-right: 40px solid transparent; 7 | 8 | border-bottom: 40px solid black; 9 | } 10 | 11 | .arrowWrapper { 12 | width: 80px; 13 | height: 40px; 14 | position: relative; 15 | background-color: white; 16 | border-color: white; 17 | } 18 | 19 | .arrowUpInner { 20 | position: absolute; 21 | top: 5px; 22 | left: 5px; 23 | width: 0; 24 | height: 0; 25 | border-left: 35px solid transparent; 26 | border-right: 35px solid transparent; 27 | 28 | border-bottom: 35px solid; 29 | border-bottom-color: inherit; 30 | } 31 | 32 | .box { 33 | width: 88px; 34 | height: 30px; 35 | border: 1px solid black; 36 | border-radius: 5px; 37 | margin-left: -5px; 38 | text-align: center; 39 | padding-top: 15px; 40 | } 41 | -------------------------------------------------------------------------------- /src/components/ping/ping.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Animation from '../animation' 3 | import { BaseProps, defaultProps } from '../utils' 4 | import css from './ping.module.css' 5 | 6 | export interface Props extends BaseProps { 7 | /** The color of the ping animation. */ 8 | color?: string 9 | /** The scale of the ping animation measured in percent. (1 = 100%) */ 10 | scale?: number 11 | } 12 | 13 | /** 14 | * A ping animation. 15 | */ 16 | const Ping = (props: Props) => { 17 | const style = { 18 | '--color': `${props.color || 'white'}`, 19 | '--scale': `${props.scale || 1.5}`, 20 | } as React.CSSProperties 21 | 22 | return ( 23 |
24 | 25 | {props.children} 26 |
27 | ) 28 | } 29 | 30 | Ping.defaultProps = { 31 | ...defaultProps, 32 | scale: 1.5, 33 | color: 'white', 34 | } 35 | 36 | export default Ping 37 | -------------------------------------------------------------------------------- /src/components/circle/circle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Animation from '../animation' 3 | import { BaseProps, defaultProps } from '../utils' 4 | import css from './circle.module.css' 5 | 6 | export interface Props extends BaseProps { 7 | /** The thickness of the line animation. */ 8 | thickness?: number 9 | /** The color of the line animation. */ 10 | color?: string 11 | } 12 | 13 | /** 14 | * A circle animation. 15 | */ 16 | const Circle = (props: Props) => { 17 | const style = { 18 | '--offset': `${props.thickness || 1}px`, 19 | '--color': `${props.color || 'white'}`, 20 | } as React.CSSProperties 21 | 22 | return ( 23 |
24 | 25 | {props.children} 26 |
27 | ) 28 | } 29 | 30 | Circle.defaultProps = { 31 | ...defaultProps, 32 | thickness: 1, 33 | color: 'white', 34 | } 35 | 36 | export default Circle 37 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Preview } from '@storybook/react' 3 | import { Title, Subtitle, Description, Primary, Controls } from '@storybook/blocks' 4 | import ReactGA from 'react-ga4' 5 | import './preview.css' 6 | 7 | if (process.env.NODE_ENV !== 'development') ReactGA.initialize('G-7P3LB8SWYT') 8 | 9 | const preview: Preview = { 10 | tags: ['autodocs'], 11 | decorators: [ 12 | (Story) => ( 13 |
14 | 15 |
16 | ), 17 | ], 18 | parameters: { 19 | controls: { 20 | matchers: { 21 | color: /(background|color)$/i, 22 | date: /Date$/i, 23 | }, 24 | }, 25 | docs: { 26 | controls: { exclude: ['children'] }, 27 | page: () => ( 28 | <> 29 | 30 | <Subtitle /> 31 | <Description /> 32 | <Primary /> 33 | <Controls /> 34 | </> 35 | ), 36 | }, 37 | }, 38 | } 39 | 40 | export default preview 41 | -------------------------------------------------------------------------------- /src/stories/flip/flip.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import Flip from '../../components/flip' 5 | 6 | const meta: Meta<typeof Flip> = { 7 | component: Flip, 8 | title: 'Components/Flip', 9 | argTypes: { 10 | children: { 11 | table: { 12 | disable: true, 13 | }, 14 | }, 15 | }, 16 | } 17 | export default meta 18 | 19 | type Story = StoryObj<typeof Flip> 20 | 21 | export const Button: Story = { 22 | args: { 23 | duration: '1s', 24 | iterations: 0, 25 | iterationDelay: '1s', 26 | reverse: false, 27 | }, 28 | 29 | render: (args) => ( 30 | <Flip {...args}> 31 | <button>Click Me!</button> 32 | </Flip> 33 | ), 34 | } 35 | 36 | export const Input: Story = { 37 | args: { 38 | duration: '1s', 39 | iterations: 0, 40 | iterationDelay: '1s', 41 | reverse: false, 42 | }, 43 | 44 | render: (args) => ( 45 | <Flip {...args}> 46 | <input placeholder='Write Something!' type='text' /> 47 | </Flip> 48 | ), 49 | } 50 | -------------------------------------------------------------------------------- /src/stories/spin/spin.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import Spin from '../../components/spin' 5 | 6 | const meta: Meta<typeof Spin> = { 7 | component: Spin, 8 | title: 'Components/Spin', 9 | argTypes: { 10 | children: { 11 | table: { 12 | disable: true, 13 | }, 14 | }, 15 | }, 16 | } 17 | export default meta 18 | 19 | type Story = StoryObj<typeof Spin> 20 | 21 | export const Button: Story = { 22 | args: { 23 | duration: '1s', 24 | iterations: 0, 25 | iterationDelay: '1s', 26 | reverse: false, 27 | }, 28 | 29 | render: (args) => ( 30 | <Spin {...args}> 31 | <button>Click Me!</button> 32 | </Spin> 33 | ), 34 | } 35 | 36 | export const Input: Story = { 37 | args: { 38 | duration: '1s', 39 | iterations: 0, 40 | iterationDelay: '1s', 41 | reverse: false, 42 | }, 43 | 44 | render: (args) => ( 45 | <Spin {...args}> 46 | <input placeholder='Write Something!' type='text' /> 47 | </Spin> 48 | ), 49 | } 50 | -------------------------------------------------------------------------------- /src/stories/flash/flash.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import Flash from '../../components/flash' 5 | 6 | const meta: Meta<typeof Flash> = { 7 | component: Flash, 8 | title: 'Components/Flash', 9 | argTypes: { 10 | children: { 11 | table: { 12 | disable: true, 13 | }, 14 | }, 15 | }, 16 | } 17 | export default meta 18 | 19 | type Story = StoryObj<typeof Flash> 20 | 21 | export const Button: Story = { 22 | args: { 23 | duration: '1s', 24 | iterations: 0, 25 | iterationDelay: '1s', 26 | reverse: false, 27 | }, 28 | 29 | render: (args) => ( 30 | <Flash {...args}> 31 | <button>Click Me!</button> 32 | </Flash> 33 | ), 34 | } 35 | 36 | export const Input: Story = { 37 | args: { 38 | duration: '1s', 39 | iterations: 0, 40 | iterationDelay: '1s', 41 | reverse: false, 42 | }, 43 | 44 | render: (args) => ( 45 | <Flash {...args}> 46 | <input placeholder='Write Something!' type='text' /> 47 | </Flash> 48 | ), 49 | } 50 | -------------------------------------------------------------------------------- /src/stories/pulse/pulse.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import Pulse from '../../components/pulse' 5 | 6 | const meta: Meta<typeof Pulse> = { 7 | component: Pulse, 8 | title: 'Components/Pulse', 9 | argTypes: { 10 | children: { 11 | table: { 12 | disable: true, 13 | }, 14 | }, 15 | }, 16 | } 17 | export default meta 18 | 19 | type Story = StoryObj<typeof Pulse> 20 | 21 | export const Button: Story = { 22 | args: { 23 | duration: '1s', 24 | iterations: 0, 25 | iterationDelay: '1s', 26 | reverse: false, 27 | }, 28 | 29 | render: (args) => ( 30 | <Pulse {...args}> 31 | <button>Click Me!</button> 32 | </Pulse> 33 | ), 34 | } 35 | 36 | export const Input: Story = { 37 | args: { 38 | duration: '1s', 39 | iterations: 0, 40 | iterationDelay: '1s', 41 | reverse: false, 42 | }, 43 | 44 | render: (args) => ( 45 | <Pulse {...args}> 46 | <input placeholder='Write Something!' type='text' /> 47 | </Pulse> 48 | ), 49 | } 50 | -------------------------------------------------------------------------------- /src/stories/shake/shake.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import Shake from '../../components/shake' 5 | 6 | const meta: Meta<typeof Shake> = { 7 | component: Shake, 8 | title: 'Components/Shake', 9 | argTypes: { 10 | children: { 11 | table: { 12 | disable: true, 13 | }, 14 | }, 15 | }, 16 | } 17 | export default meta 18 | 19 | type Story = StoryObj<typeof Shake> 20 | 21 | export const Button: Story = { 22 | args: { 23 | duration: '1s', 24 | iterations: 0, 25 | iterationDelay: '1s', 26 | reverse: false, 27 | }, 28 | 29 | render: (args) => ( 30 | <Shake {...args}> 31 | <button>Click Me!</button> 32 | </Shake> 33 | ), 34 | } 35 | 36 | export const Input: Story = { 37 | args: { 38 | duration: '1s', 39 | iterations: 0, 40 | iterationDelay: '1s', 41 | reverse: false, 42 | }, 43 | 44 | render: (args) => ( 45 | <Shake {...args}> 46 | <input placeholder='Write Something!' type='text' /> 47 | </Shake> 48 | ), 49 | } 50 | -------------------------------------------------------------------------------- /src/stories/bounce/bounce.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import Bounce from '../../components/bounce' 5 | 6 | const meta: Meta<typeof Bounce> = { 7 | component: Bounce, 8 | title: 'Components/Bounce', 9 | argTypes: { 10 | children: { 11 | table: { 12 | disable: true, 13 | }, 14 | }, 15 | }, 16 | } 17 | export default meta 18 | 19 | type Story = StoryObj<typeof Bounce> 20 | 21 | export const Button: Story = { 22 | args: { 23 | duration: '1s', 24 | iterations: 0, 25 | iterationDelay: '1s', 26 | reverse: false, 27 | }, 28 | 29 | render: (args) => ( 30 | <Bounce {...args}> 31 | <button>Click Me!</button> 32 | </Bounce> 33 | ), 34 | } 35 | 36 | export const Input: Story = { 37 | args: { 38 | duration: '1s', 39 | iterations: 0, 40 | iterationDelay: '1s', 41 | reverse: false, 42 | }, 43 | 44 | render: (args) => ( 45 | <Bounce {...args}> 46 | <input placeholder='Write Something!' type='text' /> 47 | </Bounce> 48 | ), 49 | } 50 | -------------------------------------------------------------------------------- /src/stories/wiggle/wiggle.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import Wiggle from '../../components/wiggle' 5 | 6 | const meta: Meta<typeof Wiggle> = { 7 | component: Wiggle, 8 | title: 'Components/Wiggle', 9 | argTypes: { 10 | children: { 11 | table: { 12 | disable: true, 13 | }, 14 | }, 15 | }, 16 | } 17 | export default meta 18 | 19 | type Story = StoryObj<typeof Wiggle> 20 | 21 | export const Button: Story = { 22 | args: { 23 | duration: '1s', 24 | iterations: 0, 25 | iterationDelay: '1s', 26 | reverse: false, 27 | }, 28 | 29 | render: (args) => ( 30 | <Wiggle {...args}> 31 | <button>Click Me!</button> 32 | </Wiggle> 33 | ), 34 | } 35 | 36 | export const Input: Story = { 37 | args: { 38 | duration: '1s', 39 | iterations: 0, 40 | iterationDelay: '1s', 41 | reverse: false, 42 | }, 43 | 44 | render: (args) => ( 45 | <Wiggle {...args}> 46 | <input placeholder='Write Something!' type='text' /> 47 | </Wiggle> 48 | ), 49 | } 50 | -------------------------------------------------------------------------------- /src/stories/squeeze/squeeze.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import Squeeze from '../../components/squeeze' 5 | 6 | const meta: Meta<typeof Squeeze> = { 7 | component: Squeeze, 8 | title: 'Components/Squeeze', 9 | argTypes: { 10 | children: { 11 | table: { 12 | disable: true, 13 | }, 14 | }, 15 | }, 16 | } 17 | export default meta 18 | 19 | type Story = StoryObj<typeof Squeeze> 20 | 21 | export const Button: Story = { 22 | args: { 23 | duration: '1s', 24 | iterations: 0, 25 | iterationDelay: '1s', 26 | reverse: false, 27 | }, 28 | 29 | render: (args) => ( 30 | <Squeeze {...args}> 31 | <button>Click Me!</button> 32 | </Squeeze> 33 | ), 34 | } 35 | 36 | export const Input: Story = { 37 | args: { 38 | duration: '1s', 39 | iterations: 0, 40 | iterationDelay: '1s', 41 | reverse: false, 42 | }, 43 | 44 | render: (args) => ( 45 | <Squeeze {...args}> 46 | <input placeholder='Write Something!' type='text' /> 47 | </Squeeze> 48 | ), 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Brandon McFarlin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/stories/multi/pulse-wiggle/pulse-wiggle.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import Pulse from '../../../components/pulse' 5 | import Wiggle from '../../../components/wiggle' 6 | 7 | const meta: Meta<typeof Pulse> = { 8 | component: Pulse, 9 | title: 'Components/Combinations/Pulse&Wiggle', 10 | subcomponents: { Wiggle: Wiggle as React.ComponentType<unknown> }, 11 | argTypes: { 12 | children: { 13 | table: { 14 | disable: true, 15 | }, 16 | }, 17 | }, 18 | } 19 | export default meta 20 | 21 | type Story = StoryObj<typeof Pulse> 22 | 23 | export const Button: Story = { 24 | render: (args) => ( 25 | <Pulse {...args}> 26 | <Wiggle {...args}> 27 | <button>Click Me!</button> 28 | </Wiggle> 29 | </Pulse> 30 | ), 31 | } 32 | 33 | export const Input: Story = { 34 | args: { 35 | duration: '1s', 36 | iterations: 0, 37 | iterationDelay: '1s', 38 | reverse: false, 39 | }, 40 | 41 | render: (args) => ( 42 | <Pulse {...args}> 43 | <Wiggle {...args}> 44 | <input placeholder='Write Something!' type='text' /> 45 | </Wiggle> 46 | </Pulse> 47 | ), 48 | } 49 | -------------------------------------------------------------------------------- /src/stories/multi/pulse-circle/pulse-squeeze.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import Pulse from '../../../components/pulse' 5 | import Squeeze from '../../../components/squeeze' 6 | 7 | const meta: Meta<typeof Pulse> = { 8 | component: Pulse, 9 | title: 'Components/Combinations/Pulse&Squeeze', 10 | subcomponents: { Squeeze: Squeeze as React.ComponentType<unknown> }, 11 | decorators: [ 12 | (Story) => ( 13 | <div style={{ borderRadius: '5px' }}> 14 | <Story /> 15 | </div> 16 | ), 17 | ], 18 | argTypes: { 19 | children: { 20 | table: { 21 | disable: true, 22 | }, 23 | }, 24 | }, 25 | } 26 | export default meta 27 | 28 | type Story = StoryObj<typeof Pulse> 29 | 30 | export const Button: Story = { 31 | render: (args) => ( 32 | <Squeeze {...args}> 33 | <Pulse {...args}> 34 | <button>Click Me!</button> 35 | </Pulse> 36 | </Squeeze> 37 | ), 38 | } 39 | 40 | export const Input: Story = { 41 | render: (args) => ( 42 | <Pulse {...args}> 43 | <Squeeze {...args}> 44 | <input placeholder='Write Something!' type='text' /> 45 | </Squeeze> 46 | </Pulse> 47 | ), 48 | } 49 | -------------------------------------------------------------------------------- /__tests__/components/flip.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import Flip from '../../src/components/flip' 4 | 5 | const flip = ( 6 | <Flip> 7 | <div>Flip Component</div> 8 | </Flip> 9 | ) 10 | 11 | describe('Flip component', () => { 12 | it('renders without crashing', () => { 13 | render(flip) 14 | expect(screen.getByTestId('animation-component')).toBeInTheDocument() 15 | }) 16 | 17 | it('applies the correct styles', () => { 18 | render(flip) 19 | const flipComponent = screen.getByTestId('animation-component') 20 | expect(flipComponent).toHaveStyle('--duration: 1s') 21 | expect(flipComponent).toHaveStyle('--iterations: infinite') 22 | expect(flipComponent).toHaveStyle('--direction: normal') 23 | }) 24 | 25 | it('applies modified styles', () => { 26 | render( 27 | <Flip duration='2s' iterations={2} reverse initialDelay='1s' iterationDelay='0.5s' disabled> 28 | <div>Flip Component</div> 29 | </Flip> 30 | ) 31 | const flipComponent = screen.getByTestId('animation-component') 32 | expect(flipComponent).toHaveStyle('--duration: 2s') 33 | expect(flipComponent).toHaveStyle('--iterations: 2') 34 | expect(flipComponent).toHaveStyle('--direction: reverse') 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /__tests__/components/glow.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import Glow from '../../src/components/glow' 4 | 5 | const glow = ( 6 | <Glow> 7 | <div>Glow Component</div> 8 | </Glow> 9 | ) 10 | 11 | describe('Glow component', () => { 12 | it('renders without crashing', () => { 13 | render(glow) 14 | expect(screen.getByTestId('animation-component')).toBeInTheDocument() 15 | }) 16 | 17 | it('applies the correct styles', () => { 18 | render(glow) 19 | const glowComponent = screen.getByTestId('animation-component') 20 | expect(glowComponent).toHaveStyle('--duration: 1s') 21 | expect(glowComponent).toHaveStyle('--iterations: infinite') 22 | expect(glowComponent).toHaveStyle('--direction: normal') 23 | }) 24 | 25 | it('applies modified styles', () => { 26 | render( 27 | <Glow duration='2s' iterations={2} reverse initialDelay='1s' iterationDelay='0.5s' disabled> 28 | <div>Glow Component</div> 29 | </Glow> 30 | ) 31 | const glowComponent = screen.getByTestId('animation-component') 32 | expect(glowComponent).toHaveStyle('--duration: 2s') 33 | expect(glowComponent).toHaveStyle('--iterations: 2') 34 | expect(glowComponent).toHaveStyle('--direction: reverse') 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /__tests__/components/ping.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import Ping from '../../src/components/ping' 4 | 5 | const ping = ( 6 | <Ping> 7 | <div>Ping Component</div> 8 | </Ping> 9 | ) 10 | 11 | describe('Ping component', () => { 12 | it('renders without crashing', () => { 13 | render(ping) 14 | expect(screen.getByTestId('animation-component')).toBeInTheDocument() 15 | }) 16 | 17 | it('applies the correct styles', () => { 18 | render(ping) 19 | const pingComponent = screen.getByTestId('animation-component') 20 | expect(pingComponent).toHaveStyle('--duration: 1s') 21 | expect(pingComponent).toHaveStyle('--iterations: infinite') 22 | expect(pingComponent).toHaveStyle('--direction: normal') 23 | }) 24 | 25 | it('applies modified styles', () => { 26 | render( 27 | <Ping duration='2s' iterations={2} reverse initialDelay='1s' iterationDelay='0.5s' disabled> 28 | <div>Ping Component</div> 29 | </Ping> 30 | ) 31 | const pingComponent = screen.getByTestId('animation-component') 32 | expect(pingComponent).toHaveStyle('--duration: 2s') 33 | expect(pingComponent).toHaveStyle('--iterations: 2') 34 | expect(pingComponent).toHaveStyle('--direction: reverse') 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /__tests__/components/spin.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import Spin from '../../src/components/spin' 4 | 5 | const spin = ( 6 | <Spin> 7 | <div>Spin Component</div> 8 | </Spin> 9 | ) 10 | 11 | describe('Spin component', () => { 12 | it('renders without crashing', () => { 13 | render(spin) 14 | expect(screen.getByTestId('animation-component')).toBeInTheDocument() 15 | }) 16 | 17 | it('applies the correct styles', () => { 18 | render(spin) 19 | const spinComponent = screen.getByTestId('animation-component') 20 | expect(spinComponent).toHaveStyle('--duration: 1s') 21 | expect(spinComponent).toHaveStyle('--iterations: infinite') 22 | expect(spinComponent).toHaveStyle('--direction: normal') 23 | }) 24 | 25 | it('applies modified styles', () => { 26 | render( 27 | <Spin duration='2s' iterations={2} reverse initialDelay='1s' iterationDelay='0.5s' disabled> 28 | <div>Spin Component</div> 29 | </Spin> 30 | ) 31 | const spinComponent = screen.getByTestId('animation-component') 32 | expect(spinComponent).toHaveStyle('--duration: 2s') 33 | expect(spinComponent).toHaveStyle('--iterations: 2') 34 | expect(spinComponent).toHaveStyle('--direction: reverse') 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/stories/ping/ping.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import Ping from '../../components/ping' 5 | 6 | const meta: Meta<typeof Ping> = { 7 | component: Ping, 8 | decorators: [ 9 | (Story) => ( 10 | <div style={{ borderRadius: '5px' }}> 11 | <Story /> 12 | </div> 13 | ), 14 | ], 15 | title: 'Components/Ping', 16 | argTypes: { 17 | children: { 18 | table: { 19 | disable: true, 20 | }, 21 | }, 22 | }, 23 | } 24 | export default meta 25 | 26 | type Story = StoryObj<typeof Ping> 27 | 28 | export const Button: Story = { 29 | args: { 30 | duration: '1s', 31 | iterations: 0, 32 | iterationDelay: '1s', 33 | reverse: false, 34 | color: '#e9e9e9', 35 | scale: 1.5, 36 | }, 37 | 38 | render: (args) => ( 39 | <Ping {...args}> 40 | <button>Click Me!</button> 41 | </Ping> 42 | ), 43 | } 44 | 45 | export const Input: Story = { 46 | args: { 47 | duration: '1s', 48 | iterations: 0, 49 | iterationDelay: '1s', 50 | reverse: false, 51 | color: '#e9e9e9', 52 | scale: 1.5, 53 | }, 54 | 55 | render: (args) => ( 56 | <Ping {...args}> 57 | <input placeholder='Write Something!' type='text' /> 58 | </Ping> 59 | ), 60 | } 61 | -------------------------------------------------------------------------------- /src/stories/glow/glow.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import Glow from '../../components/glow' 5 | 6 | const meta: Meta<typeof Glow> = { 7 | component: Glow, 8 | decorators: [ 9 | (Story) => ( 10 | <div style={{ borderRadius: '5px' }}> 11 | <Story /> 12 | </div> 13 | ), 14 | ], 15 | title: 'Components/Glow', 16 | argTypes: { 17 | children: { 18 | table: { 19 | disable: true, 20 | }, 21 | }, 22 | }, 23 | } 24 | export default meta 25 | 26 | type Story = StoryObj<typeof Glow> 27 | 28 | export const Button: Story = { 29 | args: { 30 | duration: '4s', 31 | iterations: 0, 32 | iterationDelay: '1s', 33 | reverse: false, 34 | thickness: 5, 35 | color: '#ff0000', 36 | initialDelay: '0s', 37 | disabled: false, 38 | }, 39 | 40 | render: (args) => ( 41 | <Glow {...args}> 42 | <button>Click Me!</button> 43 | </Glow> 44 | ), 45 | } 46 | 47 | export const Input: Story = { 48 | args: { 49 | duration: '1s', 50 | iterations: 0, 51 | iterationDelay: '1s', 52 | reverse: false, 53 | }, 54 | 55 | render: (args) => ( 56 | <Glow {...args}> 57 | <input placeholder='Write Something!' type='text' /> 58 | </Glow> 59 | ), 60 | } 61 | -------------------------------------------------------------------------------- /__tests__/components/flash.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import Flash from '../../src/components/flash' 4 | 5 | const flash = ( 6 | <Flash> 7 | <div>Flash Component</div> 8 | </Flash> 9 | ) 10 | 11 | describe('Flash component', () => { 12 | it('renders without crashing', () => { 13 | render(flash) 14 | expect(screen.getByTestId('animation-component')).toBeInTheDocument() 15 | }) 16 | 17 | it('applies the correct styles', () => { 18 | render(flash) 19 | const flashComponent = screen.getByTestId('animation-component') 20 | expect(flashComponent).toHaveStyle('--duration: 1s') 21 | expect(flashComponent).toHaveStyle('--iterations: infinite') 22 | expect(flashComponent).toHaveStyle('--direction: normal') 23 | }) 24 | 25 | it('applies modified styles', () => { 26 | render( 27 | <Flash duration='2s' iterations={2} reverse initialDelay='1s' iterationDelay='0.5s' disabled> 28 | <div>Flash Component</div> 29 | </Flash> 30 | ) 31 | const flashComponent = screen.getByTestId('animation-component') 32 | expect(flashComponent).toHaveStyle('--duration: 2s') 33 | expect(flashComponent).toHaveStyle('--iterations: 2') 34 | expect(flashComponent).toHaveStyle('--direction: reverse') 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /__tests__/components/pulse.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import Pulse from '../../src/components/pulse' 4 | 5 | const pulse = ( 6 | <Pulse> 7 | <div>Pulse Component</div> 8 | </Pulse> 9 | ) 10 | 11 | describe('Pulse component', () => { 12 | it('renders without crashing', () => { 13 | render(pulse) 14 | expect(screen.getByTestId('animation-component')).toBeInTheDocument() 15 | }) 16 | 17 | it('applies the correct styles', () => { 18 | render(pulse) 19 | const pulseComponent = screen.getByTestId('animation-component') 20 | expect(pulseComponent).toHaveStyle('--duration: 1s') 21 | expect(pulseComponent).toHaveStyle('--iterations: infinite') 22 | expect(pulseComponent).toHaveStyle('--direction: normal') 23 | }) 24 | 25 | it('applies modified styles', () => { 26 | render( 27 | <Pulse duration='2s' iterations={2} reverse initialDelay='1s' iterationDelay='0.5s' disabled> 28 | <div>Pulse Component</div> 29 | </Pulse> 30 | ) 31 | const pulseComponent = screen.getByTestId('animation-component') 32 | expect(pulseComponent).toHaveStyle('--duration: 2s') 33 | expect(pulseComponent).toHaveStyle('--iterations: 2') 34 | expect(pulseComponent).toHaveStyle('--direction: reverse') 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /__tests__/components/shake.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import Shake from '../../src/components/shake' 4 | 5 | const shake = ( 6 | <Shake> 7 | <div>Shake Component</div> 8 | </Shake> 9 | ) 10 | 11 | describe('Shake component', () => { 12 | it('renders without crashing', () => { 13 | render(shake) 14 | expect(screen.getByTestId('animation-component')).toBeInTheDocument() 15 | }) 16 | 17 | it('applies the correct styles', () => { 18 | render(shake) 19 | const shakeComponent = screen.getByTestId('animation-component') 20 | expect(shakeComponent).toHaveStyle('--duration: 1s') 21 | expect(shakeComponent).toHaveStyle('--iterations: infinite') 22 | expect(shakeComponent).toHaveStyle('--direction: normal') 23 | }) 24 | 25 | it('applies modified styles', () => { 26 | render( 27 | <Shake duration='2s' iterations={2} reverse initialDelay='1s' iterationDelay='0.5s' disabled> 28 | <div>Shake Component</div> 29 | </Shake> 30 | ) 31 | const shakeComponent = screen.getByTestId('animation-component') 32 | expect(shakeComponent).toHaveStyle('--duration: 2s') 33 | expect(shakeComponent).toHaveStyle('--iterations: 2') 34 | expect(shakeComponent).toHaveStyle('--direction: reverse') 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /__tests__/components/swing.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import Swing from '../../src/components/swing' 4 | 5 | const swing = ( 6 | <Swing> 7 | <div>Swing Component</div> 8 | </Swing> 9 | ) 10 | 11 | describe('Swing component', () => { 12 | it('renders without crashing', () => { 13 | render(swing) 14 | expect(screen.getByTestId('animation-component')).toBeInTheDocument() 15 | }) 16 | 17 | it('applies the correct styles', () => { 18 | render(swing) 19 | const swingComponent = screen.getByTestId('animation-component') 20 | expect(swingComponent).toHaveStyle('--duration: 1s') 21 | expect(swingComponent).toHaveStyle('--iterations: infinite') 22 | expect(swingComponent).toHaveStyle('--direction: normal') 23 | }) 24 | 25 | it('applies modified styles', () => { 26 | render( 27 | <Swing duration='2s' iterations={2} reverse initialDelay='1s' iterationDelay='0.5s' disabled> 28 | <div>Swing Component</div> 29 | </Swing> 30 | ) 31 | const swingComponent = screen.getByTestId('animation-component') 32 | expect(swingComponent).toHaveStyle('--duration: 2s') 33 | expect(swingComponent).toHaveStyle('--iterations: 2') 34 | expect(swingComponent).toHaveStyle('--direction: reverse') 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish-npm: 9 | if: '!github.event.release.prerelease' 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | registry-url: https://registry.npmjs.org/ 17 | - uses: pnpm/action-setup@v4 18 | with: 19 | version: 8 20 | run_install: false 21 | - name: Get pnpm store directory 22 | shell: bash 23 | run: | 24 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 25 | - uses: actions/cache@v4 26 | name: Setup pnpm cache 27 | with: 28 | path: | 29 | ${{ env.STORE_PATH }} 30 | ${{ github.workspace }}/.next/cache 31 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} 32 | restore-keys: | 33 | ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- 34 | - name: Install dependencies 35 | run: pnpm i 36 | - name: Publish to NPM 37 | run: npm publish 38 | env: 39 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 40 | -------------------------------------------------------------------------------- /src/stories/circle/circle.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import Circle from '../../components/circle' 5 | 6 | const meta: Meta<typeof Circle> = { 7 | component: Circle, 8 | decorators: [ 9 | (Story) => ( 10 | <div style={{ borderRadius: '5px' }}> 11 | <Story /> 12 | </div> 13 | ), 14 | ], 15 | title: 'Components/Circle', 16 | argTypes: { 17 | children: { 18 | table: { 19 | disable: true, 20 | }, 21 | }, 22 | }, 23 | } 24 | export default meta 25 | 26 | type Story = StoryObj<typeof Circle> 27 | 28 | export const Button: Story = { 29 | args: { 30 | thickness: 2, 31 | color: '#ff0000', 32 | iterations: 0, 33 | duration: '2s', 34 | iterationDelay: '1s', 35 | reverse: false, 36 | }, 37 | 38 | render: (args) => ( 39 | <Circle {...args}> 40 | <button>Click Me!</button> 41 | </Circle> 42 | ), 43 | } 44 | 45 | export const Input: Story = { 46 | args: { 47 | thickness: 2, 48 | color: '#ff0000', 49 | iterations: 0, 50 | duration: '2s', 51 | iterationDelay: '1s', 52 | reverse: false, 53 | }, 54 | 55 | render: (args) => ( 56 | <Circle {...args}> 57 | <input placeholder='Write Something!' type='text' /> 58 | </Circle> 59 | ), 60 | } 61 | -------------------------------------------------------------------------------- /__tests__/components/bounce.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import Bounce from '../../src/components/bounce' 4 | 5 | const bounce = ( 6 | <Bounce> 7 | <div>Bounce Component</div> 8 | </Bounce> 9 | ) 10 | 11 | describe('Bounce component', () => { 12 | it('renders without crashing', () => { 13 | render(bounce) 14 | expect(screen.getByTestId('animation-component')).toBeInTheDocument() 15 | }) 16 | 17 | it('applies the correct styles', () => { 18 | render(bounce) 19 | const bounceComponent = screen.getByTestId('animation-component') 20 | expect(bounceComponent).toHaveStyle('--duration: 1s') 21 | expect(bounceComponent).toHaveStyle('--iterations: infinite') 22 | expect(bounceComponent).toHaveStyle('--direction: normal') 23 | }) 24 | 25 | it('applies modified styles', () => { 26 | render( 27 | <Bounce duration='2s' iterations={2} reverse initialDelay='1s' iterationDelay='0.5s' disabled> 28 | <div>Bounce Component</div> 29 | </Bounce> 30 | ) 31 | const bounceComponent = screen.getByTestId('animation-component') 32 | expect(bounceComponent).toHaveStyle('--duration: 2s') 33 | expect(bounceComponent).toHaveStyle('--iterations: 2') 34 | expect(bounceComponent).toHaveStyle('--direction: reverse') 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /__tests__/components/circle.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import Circle from '../../src/components/circle' 4 | 5 | const circle = ( 6 | <Circle> 7 | <div>Circle Component</div> 8 | </Circle> 9 | ) 10 | 11 | describe('Circle component', () => { 12 | it('renders without crashing', () => { 13 | render(circle) 14 | expect(screen.getByTestId('animation-component')).toBeInTheDocument() 15 | }) 16 | 17 | it('applies the correct styles', () => { 18 | render(circle) 19 | const circleComponent = screen.getByTestId('animation-component') 20 | expect(circleComponent).toHaveStyle('--duration: 1s') 21 | expect(circleComponent).toHaveStyle('--iterations: infinite') 22 | expect(circleComponent).toHaveStyle('--direction: normal') 23 | }) 24 | 25 | it('applies modified styles', () => { 26 | render( 27 | <Circle duration='2s' iterations={2} reverse initialDelay='1s' iterationDelay='0.5s' disabled> 28 | <div>Circle Component</div> 29 | </Circle> 30 | ) 31 | const circleComponent = screen.getByTestId('animation-component') 32 | expect(circleComponent).toHaveStyle('--duration: 2s') 33 | expect(circleComponent).toHaveStyle('--iterations: 2') 34 | expect(circleComponent).toHaveStyle('--direction: reverse') 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /__tests__/components/wiggle.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import Wiggle from '../../src/components/wiggle' 4 | 5 | const wiggle = ( 6 | <Wiggle> 7 | <div>Wiggle Component</div> 8 | </Wiggle> 9 | ) 10 | 11 | describe('Wiggle component', () => { 12 | it('renders without crashing', () => { 13 | render(wiggle) 14 | expect(screen.getByTestId('animation-component')).toBeInTheDocument() 15 | }) 16 | 17 | it('applies the correct styles', () => { 18 | render(wiggle) 19 | const wiggleComponent = screen.getByTestId('animation-component') 20 | expect(wiggleComponent).toHaveStyle('--duration: 1s') 21 | expect(wiggleComponent).toHaveStyle('--iterations: infinite') 22 | expect(wiggleComponent).toHaveStyle('--direction: normal') 23 | }) 24 | 25 | it('applies modified styles', () => { 26 | render( 27 | <Wiggle duration='2s' iterations={2} reverse initialDelay='1s' iterationDelay='0.5s' disabled> 28 | <div>Wiggle Component</div> 29 | </Wiggle> 30 | ) 31 | const wiggleComponent = screen.getByTestId('animation-component') 32 | expect(wiggleComponent).toHaveStyle('--duration: 2s') 33 | expect(wiggleComponent).toHaveStyle('--iterations: 2') 34 | expect(wiggleComponent).toHaveStyle('--direction: reverse') 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/stories/multi/bounce-shake/bounce-shake.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import Bounce from '../../../components/bounce' 5 | import Shake from '../../../components/shake' 6 | 7 | const meta: Meta<typeof Bounce> = { 8 | component: Bounce, 9 | title: 'Components/Combinations/Bounce&Shake', 10 | subcomponents: { Shake: Shake as React.ComponentType<unknown> }, 11 | argTypes: { 12 | children: { 13 | table: { 14 | disable: true, 15 | }, 16 | }, 17 | }, 18 | } 19 | export default meta 20 | 21 | type Story = StoryObj<typeof Bounce> 22 | 23 | export const Button: Story = { 24 | args: { 25 | duration: '1s', 26 | iterations: 0, 27 | iterationDelay: '1s', 28 | reverse: false, 29 | }, 30 | 31 | render: (args) => ( 32 | <Bounce {...args}> 33 | <Shake {...args}> 34 | <button>Click Me!</button> 35 | </Shake> 36 | </Bounce> 37 | ), 38 | } 39 | 40 | export const Input: Story = { 41 | args: { 42 | duration: '1s', 43 | iterations: 0, 44 | iterationDelay: '1s', 45 | reverse: false, 46 | }, 47 | 48 | render: (args) => ( 49 | <Bounce {...args}> 50 | <Shake {...args}> 51 | <input placeholder='Write Something!' type='text' /> 52 | </Shake> 53 | </Bounce> 54 | ), 55 | } 56 | -------------------------------------------------------------------------------- /__tests__/components/squeeze.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import Squeeze from '../../src/components/squeeze' 4 | 5 | const squeeze = ( 6 | <Squeeze> 7 | <div>Squeeze Component</div> 8 | </Squeeze> 9 | ) 10 | 11 | describe('Squeeze component', () => { 12 | it('renders without crashing', () => { 13 | render(squeeze) 14 | expect(screen.getByTestId('animation-component')).toBeInTheDocument() 15 | }) 16 | 17 | it('applies the correct styles', () => { 18 | render(squeeze) 19 | const squeezeComponent = screen.getByTestId('animation-component') 20 | expect(squeezeComponent).toHaveStyle('--duration: 1s') 21 | expect(squeezeComponent).toHaveStyle('--iterations: infinite') 22 | expect(squeezeComponent).toHaveStyle('--direction: normal') 23 | }) 24 | 25 | it('applies modified styles', () => { 26 | render( 27 | <Squeeze duration='2s' iterations={2} reverse initialDelay='1s' iterationDelay='0.5s' disabled> 28 | <div>Squeeze Component</div> 29 | </Squeeze> 30 | ) 31 | const squeezeComponent = screen.getByTestId('animation-component') 32 | expect(squeezeComponent).toHaveStyle('--duration: 2s') 33 | expect(squeezeComponent).toHaveStyle('--iterations: 2') 34 | expect(squeezeComponent).toHaveStyle('--direction: reverse') 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /.github/workflows/storybook-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Storybook 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | paths: 8 | - 'src/**' 9 | - '.storybook/**' 10 | 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | jobs: 17 | deploy: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | - uses: pnpm/action-setup@v4 25 | with: 26 | version: 8 27 | run_install: false 28 | - name: Get pnpm store directory 29 | shell: bash 30 | run: | 31 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 32 | - uses: actions/cache@v4 33 | name: Setup pnpm cache 34 | with: 35 | path: | 36 | ${{ env.STORE_PATH }} 37 | ${{ github.workspace }}/.next/cache 38 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} 39 | restore-keys: | 40 | ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- 41 | - id: build-publish 42 | uses: bitovi/github-actions-storybook-to-github-pages@v1.0.3 43 | with: 44 | path: storybook-static 45 | install_command: pnpm install 46 | -------------------------------------------------------------------------------- /.github/workflows/chromatic.yml: -------------------------------------------------------------------------------- 1 | name: 'Chromatic' 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'src/**' 7 | - '.storybook/**' 8 | 9 | jobs: 10 | chromatic: 11 | name: Run Chromatic 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | - uses: pnpm/action-setup@v4 21 | with: 22 | version: 8 23 | run_install: false 24 | - name: Get pnpm store directory 25 | shell: bash 26 | run: | 27 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 28 | - uses: actions/cache@v4 29 | name: Setup pnpm cache 30 | with: 31 | path: | 32 | ${{ env.STORE_PATH }} 33 | ${{ github.workspace }}/.next/cache 34 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} 35 | restore-keys: | 36 | ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- 37 | - name: Install dependencies 38 | run: pnpm i 39 | - name: Run Chromatic 40 | uses: chromaui/action@latest 41 | with: 42 | projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 43 | exitOnceUploaded: true 44 | onlyChanged: true 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-subtle-nudge 2 | 3 | A collection of animations to subtly nudge users to your React components. 4 | 5 | [![NPM version](https://img.shields.io/npm/v/react-subtle-nudge.svg?style=flat)](https://npmjs.com/package/react-subtle-nudge) 6 | [![NPM downloads](https://img.shields.io/npm/dm/react-subtle-nudge.svg?style=flat)](https://npmjs.com/package/react-subtle-nudge) 7 | [![Storybook](https://raw.githubusercontent.com/storybookjs/brand/master/badge/badge-storybook.svg)](https://brandawg93.github.io/react-subtle-nudge/) 8 | [![PayPal](https://img.shields.io/badge/paypal-donate-blue?logo=paypal)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=CEYYGVB7ZZ764&item_name=react-subtle-nudge¤cy_code=USD&source=url) 9 | 10 | [Docs](https://brandawg93.github.io/react-subtle-nudge/) 11 | 12 | ## Installation 13 | 14 | npm 15 | 16 | ```bash 17 | npm install --save react-subtle-nudge 18 | ``` 19 | 20 | pnpm 21 | 22 | ```bash 23 | pnpm install --save react-subtle-nudge 24 | ``` 25 | 26 | yarn 27 | 28 | ```bash 29 | yarn add react-subtle-nudge 30 | ``` 31 | 32 | Add the following CSS to your entry file 33 | 34 | ```js 35 | import 'react-subtle-nudge/dist/index.css' 36 | ``` 37 | 38 | ## Usage 39 | 40 | Simply wrap your component in one of the animation components. 41 | 42 | ```js 43 | import { Bounce } from 'react-subtle-nudge' 44 | 45 | const BtnBounce = () => { 46 | return ( 47 | <Bounce> 48 | <button>Click Me!</button> 49 | </Bounce> 50 | ) 51 | } 52 | ``` 53 | -------------------------------------------------------------------------------- /src/components/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a time string to milliseconds. 3 | * @param time - The time string to convert. 4 | * @returns The time in milliseconds. 5 | */ 6 | export const convertToMilliseconds = (time: string): number => { 7 | const value = parseInt(time, 10) 8 | if (time.endsWith('ms')) { 9 | return value 10 | } else if (time.endsWith('s')) { 11 | return value * 1000 12 | } else { 13 | return 0 14 | } 15 | } 16 | 17 | /** 18 | * Base props for a component. 19 | */ 20 | export interface BaseProps { 21 | children?: React.ReactNode 22 | /** The duration of the animation. */ 23 | duration: string 24 | /** The number of iterations to do. (0 is infinite) */ 25 | iterations: number 26 | /** The amount of time to delay between iterations. */ 27 | iterationDelay: string 28 | /** The amount of time to delay before starting the animation. */ 29 | initialDelay: string 30 | /** Whether to reverse the animation. */ 31 | reverse: boolean 32 | /** Whether the animation is disabled. */ 33 | disabled: boolean 34 | /** Callback function triggered when the animation starts. */ 35 | onAnimationStart?: () => void 36 | /** Callback function triggered when the animation ends. */ 37 | onAnimationEnd?: () => void 38 | } 39 | 40 | /** 41 | * Default props for a component. 42 | */ 43 | export const defaultProps: BaseProps = { 44 | duration: '1s', 45 | iterations: 0, 46 | iterationDelay: '1s', 47 | initialDelay: '0s', 48 | reverse: false, 49 | disabled: false, 50 | } 51 | -------------------------------------------------------------------------------- /src/stories/swing/swing.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { Meta, StoryObj } from '@storybook/react' 3 | 4 | import signCss from './sign.module.css' 5 | 6 | import Swing from '../../components/swing' 7 | 8 | const meta: Meta<typeof Swing> = { 9 | component: Swing, 10 | title: 'Components/Swing', 11 | argTypes: { 12 | children: { 13 | table: { 14 | disable: true, 15 | }, 16 | }, 17 | }, 18 | } 19 | export default meta 20 | 21 | type Story = StoryObj<typeof Swing> 22 | 23 | export const Button: Story = { 24 | args: { 25 | duration: '1s', 26 | iterations: 0, 27 | iterationDelay: '1s', 28 | reverse: false, 29 | }, 30 | 31 | render: (args) => ( 32 | <Swing {...args}> 33 | <button>Click Me!</button> 34 | </Swing> 35 | ), 36 | } 37 | 38 | export const Input: Story = { 39 | args: { 40 | duration: '1s', 41 | iterations: 0, 42 | iterationDelay: '1s', 43 | reverse: false, 44 | }, 45 | 46 | render: (args) => ( 47 | <Swing {...args}> 48 | <input placeholder='Write Something!' type='text' /> 49 | </Swing> 50 | ), 51 | } 52 | 53 | export const Sign: Story = { 54 | args: { 55 | duration: '1s', 56 | iterations: 0, 57 | iterationDelay: '1s', 58 | reverse: false, 59 | }, 60 | 61 | render: (args) => ( 62 | <Swing {...args}> 63 | <div className={signCss.arrowWrapper}> 64 | <div className={signCss.arrowUp}></div> 65 | <div className={signCss.arrowUpInner}></div> 66 | </div> 67 | <div className={signCss.box}> 68 | <span>Test Sign!</span> 69 | </div> 70 | </Swing> 71 | ), 72 | } 73 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import typescript from '@rollup/plugin-typescript' 4 | import dts from 'rollup-plugin-dts' 5 | import postcss from 'rollup-plugin-postcss' 6 | import autoprefixer from 'autoprefixer' 7 | import terser from '@rollup/plugin-terser' 8 | import peerDepsExternal from 'rollup-plugin-peer-deps-external' 9 | import copy from 'rollup-plugin-copy' 10 | import del from 'rollup-plugin-delete' 11 | 12 | import packageJson from './package.json' assert { type: 'json' } 13 | 14 | export default [ 15 | { 16 | input: 'src/index.ts', 17 | external: ['react-dom'], 18 | output: [ 19 | { 20 | file: packageJson.main, 21 | format: 'cjs', 22 | sourcemap: true, 23 | }, 24 | { 25 | file: packageJson.module, 26 | format: 'esm', 27 | sourcemap: true, 28 | }, 29 | ], 30 | plugins: [ 31 | resolve(), 32 | commonjs(), 33 | typescript({ tsconfig: './tsconfig.json', exclude: ['**/*.stories.tsx', '**/*.test.tsx'] }), 34 | postcss({ 35 | plugins: [autoprefixer()], 36 | sourceMap: true, 37 | extract: true, 38 | minimize: true, 39 | }), 40 | terser(), 41 | peerDepsExternal(), 42 | ], 43 | }, 44 | { 45 | input: 'dist/esm/types/index.d.ts', 46 | output: [{ file: 'dist/index.d.ts', format: 'esm' }], 47 | plugins: [ 48 | copy({ targets: [{ src: 'dist/esm/*.css*', dest: 'dist' }], hook: 'buildStart' }), 49 | del({ targets: ['dist/esm/*.css*', 'dist/cjs/*.css*'], hook: 'buildEnd' }), 50 | dts(), 51 | ], 52 | external: [/\.css$/], 53 | }, 54 | ] 55 | -------------------------------------------------------------------------------- /src/components/circle/circle.module.css: -------------------------------------------------------------------------------- 1 | .outer { 2 | position: relative; 3 | border-radius: inherit; 4 | } 5 | 6 | .animation { 7 | --start-percent: 40%; 8 | --end-percent: 60%; 9 | position: absolute; 10 | top: calc(-1 * var(--offset)); 11 | left: calc(-1 * var(--offset)); 12 | height: calc(100% + 2 * var(--offset)); 13 | width: calc(100% + 2 * var(--offset)); 14 | overflow: hidden; 15 | border-radius: inherit; 16 | z-index: -1; 17 | &::before { 18 | content: ''; 19 | background: conic-gradient( 20 | from 180deg at 50% 50%, 21 | transparent var(--start-percent), 22 | var(--color) 50%, 23 | transparent var(--end-percent) 24 | ); 25 | position: absolute; 26 | top: 50%; 27 | left: 50%; 28 | aspect-ratio: 1; 29 | width: 100%; 30 | animation: rotate var(--duration) linear; 31 | animation-iteration-count: var(--iterations); 32 | animation-direction: var(--direction); 33 | animation-fill-mode: forwards; 34 | } 35 | 36 | &::after { 37 | content: ''; 38 | position: absolute; 39 | inset: var(--offset); 40 | height: calc(100% - 2 * var(--offset)); 41 | width: calc(100% - 2 * var(--offset)); 42 | } 43 | } 44 | 45 | @keyframes rotate { 46 | 0% { 47 | transform: translate(-50%, -50%) scale(1.4) rotate(0turn); 48 | --start-percent: 50%; 49 | --end-percent: 50%; 50 | opacity: 0; 51 | } 52 | 10% { 53 | --start-percent: 40%; 54 | --end-percent: 60%; 55 | opacity: 1; 56 | } 57 | 90% { 58 | --start-percent: 40%; 59 | --end-percent: 60%; 60 | opacity: 1; 61 | } 62 | 100% { 63 | transform: translate(-50%, -50%) scale(1.4) rotate(1turn); 64 | --start-percent: 50%; 65 | --end-percent: 50%; 66 | opacity: 0; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/components/animation.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { convertToMilliseconds, BaseProps, defaultProps } from './utils' 3 | import css from './animation.module.css' 4 | 5 | export interface Props extends BaseProps { 6 | onAnimationIteration?: () => void 7 | className?: string 8 | style?: React.CSSProperties 9 | hasPseudo?: boolean 10 | } 11 | 12 | const Animation = (props: Props) => { 13 | useEffect(() => { 14 | if (props.initialDelay !== '0s') { 15 | setTimeout(() => { 16 | setAnimate(true) 17 | }, convertToMilliseconds(props.initialDelay)) 18 | } else { 19 | setAnimate(true) 20 | } 21 | }, [props.iterations]) 22 | 23 | const [animate, setAnimate] = React.useState(false) 24 | const [iteration, setIteration] = React.useState(0) 25 | 26 | const style = { 27 | ...props.style, 28 | '--duration': props.duration, 29 | '--iterations': `${props.iterations || 'infinite'}`, 30 | '--direction': `${props.reverse ? 'reverse' : 'normal'}`, 31 | } as React.CSSProperties 32 | 33 | const noAnimationClassName = props.hasPseudo ? css.noPseudoAnimation : css.noAnimation 34 | const animationClassName = animate && !props.disabled ? css.baseAnimation : noAnimationClassName 35 | const className = `${animationClassName} ${!props.disabled && props.className}` 36 | 37 | const handleIteration = () => { 38 | setAnimate(false) 39 | if (!props.iterations || (props.iterations && iteration + 1 < props.iterations)) { 40 | setIteration(iteration + 1) 41 | setTimeout(() => { 42 | setAnimate(true) 43 | }, convertToMilliseconds(props.iterationDelay)) 44 | } 45 | } 46 | 47 | return ( 48 | <div 49 | data-testid='animation-component' 50 | onAnimationStart={props.onAnimationStart} 51 | onAnimationEnd={props.onAnimationEnd} 52 | onAnimationIteration={handleIteration} 53 | className={className} 54 | style={style} 55 | > 56 | {props.children} 57 | </div> 58 | ) 59 | } 60 | 61 | Animation.defaultProps = defaultProps 62 | 63 | export default Animation 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-subtle-nudge", 3 | "version": "0.1.3", 4 | "description": "A collection of animations to subtly nudge users to your React components.", 5 | "main": "dist/cjs/index.js", 6 | "module": "dist/esm/index.js", 7 | "files": [ 8 | "dist" 9 | ], 10 | "types": "dist/index.d.ts", 11 | "scripts": { 12 | "clean": "rm -rf dist coverage", 13 | "rollup": "rollup -c", 14 | "type-check": "tsc --noEmit", 15 | "lint": "eslint . --ext .ts,.tsx", 16 | "lint:fix": "eslint . --ext .ts,.tsx --fix", 17 | "format": "prettier --check .", 18 | "format:fix": "prettier --write .", 19 | "precheck": "concurrently \"npm run type-check\" \"npm run lint\" \"npm run format\" \"npm run test\"", 20 | "test": "jest", 21 | "test:watch": "jest --watch", 22 | "test:coverage": "jest --coverage", 23 | "storybook": "NODE_ENV=development storybook dev -p 6006", 24 | "build-storybook": "storybook build", 25 | "prepublishOnly": "npm run clean && npm run precheck && npm run rollup", 26 | "prepare": "husky" 27 | }, 28 | "keywords": [ 29 | "react", 30 | "animation", 31 | "nudge", 32 | "subtle" 33 | ], 34 | "author": "Brandawg93", 35 | "funding": [ 36 | { 37 | "type": "github", 38 | "url": "https://github.com/sponsors/Brandawg93" 39 | }, 40 | { 41 | "type": "paypal", 42 | "url": "https://www.paypal.com/donate/?business=CEYYGVB7ZZ764&item_name=react-subtle-nudge" 43 | } 44 | ], 45 | "license": "MIT", 46 | "devDependencies": { 47 | "@chromatic-com/storybook": "^1.5.0", 48 | "@rollup/plugin-commonjs": "^25.0.8", 49 | "@rollup/plugin-node-resolve": "^15.2.3", 50 | "@rollup/plugin-terser": "^0.4.4", 51 | "@rollup/plugin-typescript": "^11.1.6", 52 | "@storybook/addon-essentials": "^8.1.5", 53 | "@storybook/addon-interactions": "^8.1.5", 54 | "@storybook/addon-links": "^8.1.5", 55 | "@storybook/addon-webpack5-compiler-swc": "^1.0.3", 56 | "@storybook/blocks": "^8.1.5", 57 | "@storybook/react": "^8.1.5", 58 | "@storybook/react-webpack5": "^8.1.5", 59 | "@storybook/test": "^8.1.5", 60 | "@storybook/types": "^8.1.5", 61 | "@testing-library/dom": "^10.1.0", 62 | "@testing-library/jest-dom": "^6.4.5", 63 | "@testing-library/react": "^16.0.0", 64 | "@types/jest": "^29.5.12", 65 | "@types/node": "^20.12.13", 66 | "@types/react": "^18.3.3", 67 | "@types/react-dom": "^18.3.0", 68 | "@typescript-eslint/eslint-plugin": "^7.10.0", 69 | "@typescript-eslint/parser": "^7.10.0", 70 | "autoprefixer": "^10.4.19", 71 | "chromatic": "^11.5.0", 72 | "concurrently": "^8.2.2", 73 | "css-loader": "^7.1.2", 74 | "eslint": "^8.57.0", 75 | "eslint-config-prettier": "^9.1.0", 76 | "eslint-plugin-storybook": "^0.8.0", 77 | "husky": "^9.0.11", 78 | "jest": "^29.7.0", 79 | "jest-environment-jsdom": "^29.7.0", 80 | "postcss": "^8.4.38", 81 | "prettier": "^3.2.5", 82 | "react": "^18.3.1", 83 | "react-dom": "^18.3.1", 84 | "react-ga4": "^2.1.0", 85 | "rollup": "^4.18.0", 86 | "rollup-plugin-copy": "^3.5.0", 87 | "rollup-plugin-delete": "^2.0.0", 88 | "rollup-plugin-dts": "^6.1.1", 89 | "rollup-plugin-peer-deps-external": "^2.2.4", 90 | "rollup-plugin-postcss": "^4.0.2", 91 | "storybook": "^8.1.5", 92 | "style-loader": "^4.0.0", 93 | "ts-jest": "^29.1.4", 94 | "ts-node": "^10.9.2", 95 | "tslib": "^2.6.2", 96 | "typescript": "^5.4.5" 97 | }, 98 | "peerDependencies": { 99 | "react": "^16 || ^17 || ^18" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | import type { Config } from 'jest' 7 | 8 | const config: Config = { 9 | // All imported modules in your tests should be mocked automatically 10 | // automock: false, 11 | 12 | // Stop running tests after `n` failures 13 | // bail: 0, 14 | 15 | // The directory where Jest should store its cached dependency information 16 | // cacheDirectory: "/private/var/folders/1z/lfx09ykx6v55w0sc8m31d38r0000gn/T/jest_dx", 17 | 18 | // Automatically clear mock calls, instances, contexts and results before every test 19 | clearMocks: true, 20 | 21 | // Indicates whether the coverage information should be collected while executing the test 22 | collectCoverage: true, 23 | 24 | // An array of glob patterns indicating a set of files for which coverage information should be collected 25 | collectCoverageFrom: ['src/components/**/*'], 26 | 27 | // The directory where Jest should output its coverage files 28 | coverageDirectory: 'coverage', 29 | 30 | // An array of regexp pattern strings used to skip coverage collection 31 | // coveragePathIgnorePatterns: [ 32 | // "/node_modules/" 33 | // ], 34 | 35 | // Indicates which provider should be used to instrument code for coverage 36 | // coverageProvider: "babel", 37 | 38 | // A list of reporter names that Jest uses when writing coverage reports 39 | // coverageReporters: [ 40 | // "json", 41 | // "text", 42 | // "lcov", 43 | // "clover" 44 | // ], 45 | 46 | // An object that configures minimum threshold enforcement for coverage results 47 | // coverageThreshold: undefined, 48 | 49 | // A path to a custom dependency extractor 50 | // dependencyExtractor: undefined, 51 | 52 | // Make calling deprecated APIs throw helpful error messages 53 | // errorOnDeprecated: false, 54 | 55 | // The default configuration for fake timers 56 | // fakeTimers: { 57 | // "enableGlobally": false 58 | // }, 59 | 60 | // Force coverage collection from ignored files using an array of glob patterns 61 | // forceCoverageMatch: [], 62 | 63 | // A path to a module which exports an async function that is triggered once before all test suites 64 | // globalSetup: undefined, 65 | 66 | // A path to a module which exports an async function that is triggered once after all test suites 67 | // globalTeardown: undefined, 68 | 69 | // A set of global variables that need to be available in all test environments 70 | // globals: {}, 71 | 72 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 73 | // maxWorkers: "50%", 74 | 75 | // An array of directory names to be searched recursively up from the requiring module's location 76 | // moduleDirectories: [ 77 | // "node_modules" 78 | // ], 79 | 80 | // An array of file extensions your modules use 81 | // moduleFileExtensions: [ 82 | // "js", 83 | // "mjs", 84 | // "cjs", 85 | // "jsx", 86 | // "ts", 87 | // "tsx", 88 | // "json", 89 | // "node" 90 | // ], 91 | 92 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 93 | // moduleNameMapper: {}, 94 | 95 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 96 | // modulePathIgnorePatterns: [], 97 | 98 | // Activates notifications for test results 99 | // notify: false, 100 | 101 | // An enum that specifies notification mode. Requires { notify: true } 102 | // notifyMode: "failure-change", 103 | 104 | // A preset that is used as a base for Jest's configuration 105 | preset: 'ts-jest', 106 | 107 | // Run tests from one or more projects 108 | // projects: undefined, 109 | 110 | // Use this configuration option to add custom reporters to Jest 111 | // reporters: undefined, 112 | 113 | // Automatically reset mock state before every test 114 | // resetMocks: false, 115 | 116 | // Reset the module registry before running each individual test 117 | // resetModules: false, 118 | 119 | // A path to a custom resolver 120 | // resolver: undefined, 121 | 122 | // Automatically restore mock state and implementation before every test 123 | // restoreMocks: false, 124 | 125 | // The root directory that Jest should scan for tests and modules within 126 | // rootDir: undefined, 127 | 128 | // A list of paths to directories that Jest should use to search for files in 129 | // roots: [ 130 | // "<rootDir>" 131 | // ], 132 | 133 | // Allows you to use a custom runner instead of Jest's default test runner 134 | // runner: "jest-runner", 135 | 136 | // The paths to modules that run some code to configure or set up the testing environment before each test 137 | // setupFiles: [], 138 | 139 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 140 | setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'], 141 | 142 | // The number of seconds after which a test is considered as slow and reported as such in the results. 143 | // slowTestThreshold: 5, 144 | 145 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 146 | // snapshotSerializers: [], 147 | 148 | // The test environment that will be used for testing 149 | testEnvironment: 'jsdom', 150 | 151 | // Options that will be passed to the testEnvironment 152 | // testEnvironmentOptions: {}, 153 | 154 | // Adds a location field to test results 155 | // testLocationInResults: false, 156 | 157 | // The glob patterns Jest uses to detect test files 158 | testMatch: [ 159 | '**/__tests__/**/*.[jt]s?(x)', 160 | // "**/?(*.)+(spec|test).[tj]s?(x)" 161 | ], 162 | 163 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 164 | // testPathIgnorePatterns: [ 165 | // "/node_modules/" 166 | // ], 167 | 168 | // The regexp pattern or array of patterns that Jest uses to detect test files 169 | // testRegex: [], 170 | 171 | // This option allows the use of a custom results processor 172 | // testResultsProcessor: undefined, 173 | 174 | // This option allows use of a custom test runner 175 | // testRunner: "jest-circus/runner", 176 | 177 | // A map from regular expressions to paths to transformers 178 | // transform: undefined, 179 | 180 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 181 | // transformIgnorePatterns: [ 182 | // "/node_modules/", 183 | // "\\.pnp\\.[^\\/]+$" 184 | // ], 185 | 186 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 187 | // unmockedModulePathPatterns: undefined, 188 | 189 | // Indicates whether each individual test should be reported during the run 190 | // verbose: undefined, 191 | 192 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 193 | // watchPathIgnorePatterns: [], 194 | 195 | // Whether to use watchman for file crawling 196 | // watchman: true, 197 | moduleNameMapper: { 198 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 199 | '<rootDir>/__mocks__/fileMock.js', 200 | '\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js', 201 | }, 202 | } 203 | 204 | export default config 205 | --------------------------------------------------------------------------------