├── .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 |
31 |
32 |
33 |
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 = {
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
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 |
31 | Click Me!
32 |
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 |
46 |
47 |
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 = {
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
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 |
31 | Click Me!
32 |
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 |
46 |
47 |
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 = {
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
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 |
31 | Click Me!
32 |
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 |
46 |
47 |
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 = {
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
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 |
31 | Click Me!
32 |
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 |
46 |
47 |
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 = {
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
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 |
31 | Click Me!
32 |
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 |
46 |
47 |
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 = {
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
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 |
31 | Click Me!
32 |
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 |
46 |
47 |
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 = {
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
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 |
31 | Click Me!
32 |
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 |
46 |
47 |
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 = {
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
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 |
31 | Click Me!
32 |
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 |
46 |
47 |
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 = {
8 | component: Pulse,
9 | title: 'Components/Combinations/Pulse&Wiggle',
10 | subcomponents: { Wiggle: Wiggle as React.ComponentType },
11 | argTypes: {
12 | children: {
13 | table: {
14 | disable: true,
15 | },
16 | },
17 | },
18 | }
19 | export default meta
20 |
21 | type Story = StoryObj
22 |
23 | export const Button: Story = {
24 | render: (args) => (
25 |
26 |
27 | Click Me!
28 |
29 |
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 |
43 |
44 |
45 |
46 |
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 = {
8 | component: Pulse,
9 | title: 'Components/Combinations/Pulse&Squeeze',
10 | subcomponents: { Squeeze: Squeeze as React.ComponentType },
11 | decorators: [
12 | (Story) => (
13 |
14 |
15 |
16 | ),
17 | ],
18 | argTypes: {
19 | children: {
20 | table: {
21 | disable: true,
22 | },
23 | },
24 | },
25 | }
26 | export default meta
27 |
28 | type Story = StoryObj
29 |
30 | export const Button: Story = {
31 | render: (args) => (
32 |
33 |
34 | Click Me!
35 |
36 |
37 | ),
38 | }
39 |
40 | export const Input: Story = {
41 | render: (args) => (
42 |
43 |
44 |
45 |
46 |
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 |
7 | Flip Component
8 |
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 |
28 | Flip Component
29 |
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 |
7 | Glow Component
8 |
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 |
28 | Glow Component
29 |
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 |
7 | Ping Component
8 |
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 |
28 | Ping Component
29 |
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 |
7 | Spin Component
8 |
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 |
28 | Spin Component
29 |
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 = {
7 | component: Ping,
8 | decorators: [
9 | (Story) => (
10 |
11 |
12 |
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
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 |
40 | Click Me!
41 |
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 |
57 |
58 |
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 = {
7 | component: Glow,
8 | decorators: [
9 | (Story) => (
10 |
11 |
12 |
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
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 |
42 | Click Me!
43 |
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 |
57 |
58 |
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 |
7 | Flash Component
8 |
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 |
28 | Flash Component
29 |
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 |
7 | Pulse Component
8 |
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 |
28 | Pulse Component
29 |
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 |
7 | Shake Component
8 |
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 |
28 | Shake Component
29 |
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 |
7 | Swing Component
8 |
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 |
28 | Swing Component
29 |
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 = {
7 | component: Circle,
8 | decorators: [
9 | (Story) => (
10 |
11 |
12 |
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
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 |
40 | Click Me!
41 |
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 |
57 |
58 |
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 |
7 | Bounce Component
8 |
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 |
28 | Bounce Component
29 |
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 |
7 | Circle Component
8 |
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 |
28 | Circle Component
29 |
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 |
7 | Wiggle Component
8 |
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 |
28 | Wiggle Component
29 |
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 = {
8 | component: Bounce,
9 | title: 'Components/Combinations/Bounce&Shake',
10 | subcomponents: { Shake: Shake as React.ComponentType },
11 | argTypes: {
12 | children: {
13 | table: {
14 | disable: true,
15 | },
16 | },
17 | },
18 | }
19 | export default meta
20 |
21 | type Story = StoryObj
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 |
33 |
34 | Click Me!
35 |
36 |
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 |
50 |
51 |
52 |
53 |
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 |
7 | Squeeze Component
8 |
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 |
28 | Squeeze Component
29 |
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 | [](https://npmjs.com/package/react-subtle-nudge)
6 | [](https://npmjs.com/package/react-subtle-nudge)
7 | [](https://brandawg93.github.io/react-subtle-nudge/)
8 | [](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 |
48 | Click Me!
49 |
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 = {
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
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 |
33 | Click Me!
34 |
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 |
48 |
49 |
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 |
63 |
67 |
68 | Test Sign!
69 |
70 |
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 |
56 | {props.children}
57 |
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 | // ""
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: ['/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 | '/__mocks__/fileMock.js',
200 | '\\.(css|less)$': '/__mocks__/styleMock.js',
201 | },
202 | }
203 |
204 | export default config
205 |
--------------------------------------------------------------------------------