├── src ├── index.d.ts ├── hooks │ └── useIsomorphicLayoutEffect.ts ├── theme │ ├── info.css │ ├── plain.css │ ├── failure.css │ ├── success.css │ ├── warning.css │ ├── sunset.css │ ├── moonlight.css │ ├── pink-dawn.css │ ├── blue-dusk.css │ ├── ocean-wave.css │ ├── dark.css │ ├── frosted-glass.css │ ├── dark-edge.css │ ├── light.css │ ├── light-edge.css │ └── chroma.css ├── lib │ ├── generateElement.ts │ ├── type-guard.ts │ ├── constants.ts │ ├── utils.ts │ └── react-render.ts ├── style.scss ├── type │ └── common.ts ├── component │ ├── toast-container.tsx │ └── toast-message.tsx └── index.tsx ├── .browserslistrc ├── setupTests.js ├── example ├── src │ ├── page │ │ ├── api │ │ │ ├── index.ts │ │ │ ├── api.module.css │ │ │ └── api.tsx │ │ ├── home │ │ │ ├── index.ts │ │ │ ├── home.module.css │ │ │ └── home.tsx │ │ ├── theme │ │ │ ├── index.ts │ │ │ ├── theme.module.css │ │ │ └── theme.tsx │ │ ├── example │ │ │ ├── index.ts │ │ │ ├── example.module.css │ │ │ └── example.tsx │ │ ├── preview │ │ │ ├── index.ts │ │ │ ├── section │ │ │ │ ├── index.ts │ │ │ │ ├── section.module.css │ │ │ │ └── section.tsx │ │ │ ├── interaction │ │ │ │ ├── index.ts │ │ │ │ ├── interaction.module.css │ │ │ │ └── interaction.tsx │ │ │ ├── preview.module.css │ │ │ └── preview.tsx │ │ ├── change-log │ │ │ ├── index.ts │ │ │ ├── change-log.module.css │ │ │ └── change-log.tsx │ │ └── getting-started │ │ │ ├── index.ts │ │ │ ├── getting-started.module.css │ │ │ └── getting-started.tsx │ ├── vite-env.d.ts │ ├── component │ │ ├── button │ │ │ ├── index.ts │ │ │ ├── button.tsx │ │ │ └── button.module.css │ │ ├── layout │ │ │ ├── index.ts │ │ │ ├── layout.module.css │ │ │ └── layout.tsx │ │ ├── example │ │ │ └── my-message │ │ │ │ ├── index.ts │ │ │ │ ├── my-message.module.css │ │ │ │ └── my-message.tsx │ │ └── common-highlighter │ │ │ ├── index.ts │ │ │ ├── common-highlighter.module.css │ │ │ └── common-highlighter.tsx │ ├── type │ │ └── utils.ts │ ├── assets │ │ └── images │ │ │ └── common │ │ │ ├── logo.png │ │ │ └── github-icon.png │ ├── root.tsx │ ├── main.tsx │ ├── util │ │ └── debounce.ts │ ├── app.tsx │ └── index.css ├── public │ ├── robots.txt │ └── favicon.ico ├── tsconfig.node.json ├── vite.config.ts ├── .gitignore ├── tsconfig.json ├── index.html └── package.json ├── docs ├── preview.gif ├── theme_creative.gif └── theme_standard.gif ├── .vscode └── settings.json ├── .prettierrc ├── babel.config.js ├── postcss.config.js ├── .gitignore ├── jest.config.js ├── .github └── workflows │ ├── 03_contribute_list.yml │ ├── 99_codereview.yml │ ├── 02_publish_to_npm.yml │ ├── 01_auto_release.yml │ ├── 01_deploy_to_github_pages.yml │ ├── 00_test.yml │ └── 02_publish_to_github.yml ├── tsconfig.json ├── .eslintrc.json ├── rollup.config.mjs ├── package.json ├── __tests__ ├── create-toast.test.tsx └── toast.test.tsx └── README.md /src/index.d.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 versions 2 | IE 11 3 | -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /example/src/page/api/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './api'; 2 | -------------------------------------------------------------------------------- /example/src/page/home/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './home'; 2 | -------------------------------------------------------------------------------- /example/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/src/page/theme/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './theme'; 2 | -------------------------------------------------------------------------------- /example/src/component/button/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './button'; 2 | -------------------------------------------------------------------------------- /example/src/component/layout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './layout'; 2 | -------------------------------------------------------------------------------- /example/src/page/example/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './example'; 2 | -------------------------------------------------------------------------------- /example/src/page/preview/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './preview'; 2 | -------------------------------------------------------------------------------- /example/src/page/change-log/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './change-log'; 2 | -------------------------------------------------------------------------------- /example/src/page/preview/section/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './section'; 2 | -------------------------------------------------------------------------------- /example/src/component/example/my-message/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './my-message'; 2 | -------------------------------------------------------------------------------- /example/src/page/getting-started/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './getting-started'; 2 | -------------------------------------------------------------------------------- /example/src/page/preview/interaction/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './interaction'; 2 | -------------------------------------------------------------------------------- /example/src/type/utils.ts: -------------------------------------------------------------------------------- 1 | export type AtLeast = Partial & Pick 2 | -------------------------------------------------------------------------------- /docs/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almond-bongbong/react-simple-toasts/HEAD/docs/preview.gif -------------------------------------------------------------------------------- /example/src/component/common-highlighter/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './common-highlighter'; 2 | -------------------------------------------------------------------------------- /example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /docs/theme_creative.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almond-bongbong/react-simple-toasts/HEAD/docs/theme_creative.gif -------------------------------------------------------------------------------- /docs/theme_standard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almond-bongbong/react-simple-toasts/HEAD/docs/theme_standard.gif -------------------------------------------------------------------------------- /example/src/page/preview/section/section.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | height: 100vh; 4 | } 5 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almond-bongbong/react-simple-toasts/HEAD/example/public/favicon.ico -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescriptreact]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /example/src/assets/images/common/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almond-bongbong/react-simple-toasts/HEAD/example/src/assets/images/common/logo.png -------------------------------------------------------------------------------- /example/src/assets/images/common/github-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/almond-bongbong/react-simple-toasts/HEAD/example/src/assets/images/common/github-icon.png -------------------------------------------------------------------------------- /example/src/page/home/home.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | padding-left: 20px; 3 | list-style: disc; 4 | } 5 | 6 | .features li + li { 7 | margin-top: 10px; 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "printWidth": 100 8 | } 9 | -------------------------------------------------------------------------------- /example/src/page/getting-started/getting-started.module.css: -------------------------------------------------------------------------------- 1 | .benefits { 2 | padding-left: 24px; 3 | list-style: disc; 4 | } 5 | 6 | .code { 7 | overflow-x: auto; 8 | } 9 | -------------------------------------------------------------------------------- /example/src/page/preview/interaction/interaction.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | } 7 | -------------------------------------------------------------------------------- /example/src/component/example/my-message/my-message.module.css: -------------------------------------------------------------------------------- 1 | .message { 2 | padding: 10px 20px; 3 | background-color: rgba(255, 107, 129, 0.9); 4 | border-radius: 4px; 5 | color: #fff; 6 | } 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ["@babel/preset-react", { runtime: "classic" }], 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const config = ({ env }) => { 2 | const isThemeEnv = env === 'theme'; 3 | 4 | if (!isThemeEnv) return {}; 5 | 6 | return { 7 | map: false, 8 | }; 9 | }; 10 | 11 | module.exports = config; 12 | -------------------------------------------------------------------------------- /src/hooks/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const useIsomorphicLayoutEffect = 4 | typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; 5 | 6 | export default useIsomorphicLayoutEffect; 7 | -------------------------------------------------------------------------------- /example/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/theme/info.css: -------------------------------------------------------------------------------- 1 | .toast__info { 2 | min-width: 200px; 3 | padding: 10px 20px; 4 | border-radius: 5px; 5 | background-color: #2196f3; 6 | box-shadow: 0 0 7px rgba(33, 150, 243, 0.5); 7 | color: #fff; 8 | text-align: center; 9 | } 10 | -------------------------------------------------------------------------------- /src/theme/plain.css: -------------------------------------------------------------------------------- 1 | .toast__plain { 2 | min-width: 200px; 3 | padding: 10px 20px; 4 | border-radius: 5px; 5 | background-color: #fff; 6 | box-shadow: 0 1px 7px rgba(0, 0, 0, 0.2); 7 | color: #333; 8 | text-align: center; 9 | } 10 | -------------------------------------------------------------------------------- /src/theme/failure.css: -------------------------------------------------------------------------------- 1 | .toast__failure { 2 | min-width: 200px; 3 | padding: 10px 20px; 4 | border-radius: 5px; 5 | background-color: #d32f2f; 6 | box-shadow: 0 0 7px rgba(211, 47, 47, 0.5); 7 | color: #fff; 8 | text-align: center; 9 | } 10 | -------------------------------------------------------------------------------- /src/theme/success.css: -------------------------------------------------------------------------------- 1 | .toast__success { 2 | min-width: 200px; 3 | padding: 10px 20px; 4 | border-radius: 5px; 5 | background-color: #43a047; 6 | box-shadow: 0 0 7px rgba(67, 160, 71, 0.5); 7 | color: #fff; 8 | text-align: center; 9 | } 10 | -------------------------------------------------------------------------------- /src/theme/warning.css: -------------------------------------------------------------------------------- 1 | .toast__warning { 2 | min-width: 200px; 3 | padding: 10px 20px; 4 | border-radius: 5px; 5 | background-color: #f0ad4e; 6 | box-shadow: 0 0 7px rgba(240, 173, 78, 0.5); 7 | color: #fff; 8 | text-align: center; 9 | } 10 | -------------------------------------------------------------------------------- /example/src/page/example/example.module.css: -------------------------------------------------------------------------------- 1 | .code { 2 | overflow-x: auto; 3 | margin-top: 20px; 4 | } 5 | 6 | .area + .area { 7 | margin-top: 40px; 8 | } 9 | 10 | .description { 11 | margin-top: 20px; 12 | } 13 | 14 | .playground { 15 | margin-top: 20px; 16 | } 17 | -------------------------------------------------------------------------------- /src/theme/sunset.css: -------------------------------------------------------------------------------- 1 | .toast__sunset { 2 | min-width: 200px; 3 | padding: 10px 20px; 4 | border-radius: 5px; 5 | background: linear-gradient(to bottom, #ffb6c1, #ff6347); 6 | box-shadow: 0 0 10px rgba(255, 99, 71, 0.5); 7 | color: #fff; 8 | text-align: center; 9 | } 10 | -------------------------------------------------------------------------------- /src/theme/moonlight.css: -------------------------------------------------------------------------------- 1 | .toast__moonlight { 2 | min-width: 200px; 3 | padding: 10px 20px; 4 | border-radius: 5px; 5 | background: linear-gradient(to bottom, #1c1c1c, #535353); 6 | box-shadow: 0 0 10px rgba(83, 83, 83, 0.5); 7 | color: #fff; 8 | text-align: center; 9 | } 10 | -------------------------------------------------------------------------------- /example/src/root.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from './component/layout'; 3 | import { Outlet } from 'react-router-dom'; 4 | 5 | function Root() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default Root; 14 | -------------------------------------------------------------------------------- /example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | build: { 8 | outDir: 'build', 9 | }, 10 | base: './', 11 | }); 12 | -------------------------------------------------------------------------------- /src/theme/pink-dawn.css: -------------------------------------------------------------------------------- 1 | .toast__pink-dawn { 2 | min-width: 200px; 3 | padding: 10px 20px; 4 | border-radius: 5px; 5 | background: linear-gradient(135deg, #f686bd 50%, #fe5d9f 100%); 6 | box-shadow: 0 0 7px rgba(0, 0, 0, 0.2); 7 | color: #fff; 8 | text-align: center; 9 | } 10 | -------------------------------------------------------------------------------- /src/theme/blue-dusk.css: -------------------------------------------------------------------------------- 1 | .toast__blue-dusk { 2 | min-width: 200px; 3 | padding: 10px 20px; 4 | border-radius: 5px; 5 | background: linear-gradient(130deg, #4e51d8 0%, #626ed4 50%, #4e51d8 100%); 6 | box-shadow: 0 0 7px rgba(0, 0, 0, 0.2); 7 | color: #fff; 8 | text-align: center; 9 | } 10 | -------------------------------------------------------------------------------- /example/src/component/common-highlighter/common-highlighter.module.css: -------------------------------------------------------------------------------- 1 | .code { 2 | display: inline-block; 3 | min-width: 100%; 4 | margin: auto; 5 | padding: 20px; 6 | font-size: 16px; 7 | white-space: pre-wrap; 8 | } 9 | 10 | @media screen and (max-width: 1200px) { 11 | .code { 12 | font-size: 14px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/theme/ocean-wave.css: -------------------------------------------------------------------------------- 1 | .toast__ocean-wave { 2 | min-width: 200px; 3 | padding: 10px 20px; 4 | border-radius: 5px; 5 | background-color: #1a237e; 6 | background-image: radial-gradient(circle at top right, #64b5f6, #1a237e); 7 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 8 | color: #ffffff; 9 | text-align: center; 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/generateElement.ts: -------------------------------------------------------------------------------- 1 | export const createElement = (id: string): HTMLElement => { 2 | const element = document.createElement('div'); 3 | element.setAttribute('id', id); 4 | return element; 5 | }; 6 | 7 | export const addRootElement = (rootElem: HTMLElement) => { 8 | document.body.appendChild(rootElem); 9 | return rootElem; 10 | }; 11 | -------------------------------------------------------------------------------- /example/src/component/example/my-message/my-message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './my-message.module.css'; 3 | 4 | interface Props { 5 | children: React.ReactNode; 6 | } 7 | 8 | function MyMessage({ children }: Props) { 9 | return
{children}
; 10 | } 11 | 12 | export default MyMessage; 13 | -------------------------------------------------------------------------------- /example/src/component/button/button.tsx: -------------------------------------------------------------------------------- 1 | import React, { ButtonHTMLAttributes } from 'react'; 2 | import styles from './button.module.css'; 3 | 4 | function Button(props: ButtonHTMLAttributes) { 5 | return ( 6 | 21 | 22 | ); 23 | } 24 | 25 | export default Interaction; 26 | -------------------------------------------------------------------------------- /.github/workflows/02_publish_to_npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish NPM Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v3 13 | 14 | - name: Set up Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: '20.x' 18 | registry-url: 'https://registry.npmjs.org' 19 | 20 | - name: Install dependencies 21 | run: yarn install 22 | 23 | - name: Build package 24 | run: yarn build 25 | 26 | - name: Publish to npm 27 | run: npm publish 28 | env: 29 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 30 | -------------------------------------------------------------------------------- /example/src/component/button/button.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | display: inline-block; 3 | padding: 11px 20px; 4 | outline: 0; 5 | border: 1px solid #0075f2; 6 | border-radius: 3px; 7 | background-color: #0075f2; 8 | color: #fff; 9 | font-size: 16px; 10 | text-align: center; 11 | cursor: pointer; 12 | white-space: nowrap; 13 | } 14 | 15 | .button:active { 16 | opacity: 0.9; 17 | } 18 | 19 | .button:disabled { 20 | border: 1px solid #ddd; 21 | background-color: #f5f5f5; 22 | color: #bbb; 23 | cursor: not-allowed; 24 | } 25 | 26 | .button + .button { 27 | margin-left: 8px; 28 | } 29 | 30 | @media screen and (max-width: 1200px) { 31 | .button { 32 | padding: 10px 15px; 33 | font-size: 14px; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "outDir": "lib", 5 | "module": "esnext", 6 | "lib": ["es6", "dom", "es2016", "es2017"], 7 | "jsx": "preserve", 8 | "declaration": true, 9 | "declarationDir": "lib", 10 | "moduleResolution": "node", 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "esModuleInterop": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "allowSyntheticDefaultImports": true, 19 | "resolveJsonModule": true 20 | }, 21 | "include": ["src"], 22 | "exclude": ["./node_modules", "./dist", "./example", "./rollup.config.mjs"], 23 | "types": ["@testing-library/dom"] 24 | } 25 | -------------------------------------------------------------------------------- /example/src/page/preview/preview.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | height: 100%; 3 | background: linear-gradient(15deg, #a6c1ff, #ffffff 60%, #ffffff); 4 | } 5 | 6 | .button_wrap { 7 | padding: 10px; 8 | } 9 | 10 | .container { 11 | box-sizing: content-box; 12 | position: absolute; 13 | top: 50%; 14 | left: 50%; 15 | width: 1020px; 16 | /*guide-line*/ 17 | /*border: 1px solid red;*/ 18 | padding-bottom: 10px; 19 | transform: translate(-50%, -50%); 20 | } 21 | 22 | .container iframe { 23 | width: 100%; 24 | height: 100%; 25 | border: 0; 26 | } 27 | 28 | .size { 29 | position: absolute; 30 | top: -24px; 31 | left: 0; 32 | color: #666; 33 | font-size: 14px; 34 | } 35 | 36 | .section { 37 | float: left; 38 | width: 25%; 39 | height: 120px; 40 | } 41 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "globals": { 12 | "Atomics": "readonly", 13 | "SharedArrayBuffer": "readonly" 14 | }, 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "jsx": true 19 | }, 20 | "ecmaVersion": 2018, 21 | "sourceType": "module" 22 | }, 23 | "plugins": ["react", "react-hooks", "@typescript-eslint"], 24 | "rules": { 25 | "react-hooks/rules-of-hooks": "error", 26 | "react-hooks/exhaustive-deps": "error" 27 | }, 28 | "ignorePatterns": ["postcss.config.js"] 29 | } 30 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | React Simple Toasts 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const SET_TIMEOUT_MAX = 2147483647; 2 | 3 | export const ToastPosition = { 4 | BOTTOM_LEFT: 'bottom-left', 5 | BOTTOM_CENTER: 'bottom-center', 6 | BOTTOM_RIGHT: 'bottom-right', 7 | TOP_LEFT: 'top-left', 8 | TOP_CENTER: 'top-center', 9 | TOP_RIGHT: 'top-right', 10 | CENTER: 'center', 11 | } as const; 12 | 13 | export const Themes = { 14 | DARK: 'dark', 15 | DARK_EDGE: 'dark-edge', 16 | LIGHT: 'light', 17 | LIGHT_EDGE: 'light-edge', 18 | PINK_DAWN: 'pink-dawn', 19 | CHROMA: 'chroma', 20 | BLUE_DUSK: 'blue-dusk', 21 | OCEAN_WAVE: 'ocean-wave', 22 | SUNSET: 'sunset', 23 | MOONLIGHT: 'moonlight', 24 | FROSTED_GLASS: 'frosted-glass', 25 | SUCCESS: 'success', 26 | INFO: 'info', 27 | WARNING: 'warning', 28 | FAILURE: 'failure', 29 | PLAIN: 'plain', 30 | } as const; 31 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | export const createId = () => Date.now() + Math.floor(Math.random() * 10000000000000000); 2 | 3 | export const isBrowser = () => typeof window !== 'undefined'; 4 | 5 | export const reverse = (arr: T[]) => { 6 | const result = []; 7 | for (let i = arr.length - 1; i >= 0; i--) { 8 | result.push(arr[i]); 9 | } 10 | return result; 11 | }; 12 | 13 | export const rgbToRgba = (rgb: string, alpha: number) => { 14 | const [r, g, b] = rgb 15 | .replace(/[rgb(]|[)]/g, '') 16 | .split(',') 17 | .map((v) => v.trim()); 18 | return `rgba(${r}, ${g}, ${b}, ${alpha})`; 19 | }; 20 | 21 | export const classes = (...args: (string | undefined)[]) => args.filter(Boolean).join(' '); 22 | 23 | export const generateMessage = () => { 24 | return `message ${Math.random().toString(36).substring(2) + Date.now()}`; 25 | }; 26 | -------------------------------------------------------------------------------- /.github/workflows/01_auto_release.yml: -------------------------------------------------------------------------------- 1 | name: Auto Release 2 | 3 | on: 4 | repository_dispatch: 5 | types: [test-done] 6 | 7 | jobs: 8 | create_release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v3 13 | 14 | - name: Get version from package.json 15 | id: version 16 | run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_ENV 17 | 18 | - name: Create Release 19 | id: create_release 20 | uses: actions/create-release@v1 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 23 | with: 24 | tag_name: v${{ env.version }} 25 | release_name: Release v${{ env.version }} 26 | body: Auto-generated release 27 | draft: false 28 | prerelease: false 29 | -------------------------------------------------------------------------------- /example/src/page/preview/section/section.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useLayoutEffect } from "react"; 2 | import { useLocation } from "react-router-dom"; 3 | import styles from "./section.module.css"; 4 | import toast, { Theme } from "react-simple-toasts"; 5 | 6 | function Section() { 7 | const { search } = useLocation(); 8 | const params = new URLSearchParams(search); 9 | const theme = params.get('theme') as Theme; 10 | 11 | useLayoutEffect(() => { 12 | document.body.style.background = 'transparent'; 13 | }, []); 14 | 15 | useEffect(() => { 16 | let count = 0; 17 | 18 | const intervalId = window.setInterval(() => { 19 | toast(theme, { theme }); 20 | count += 1; 21 | 22 | if (count === 1) { 23 | window.clearInterval(intervalId); 24 | } 25 | }, 1000); 26 | }, [theme]); 27 | 28 | return
; 29 | } 30 | 31 | export default Section; 32 | -------------------------------------------------------------------------------- /example/src/component/common-highlighter/common-highlighter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Highlight, themes } from 'prism-react-renderer'; 3 | import styles from './common-highlighter.module.css'; 4 | 5 | interface Props { 6 | children: string; 7 | language?: string; 8 | } 9 | 10 | function CommonHighlighter({ children, language = 'jsx' }: Props) { 11 | return ( 12 | 13 | {({ style, tokens, getLineProps, getTokenProps }) => ( 14 |
15 |           {tokens.map((line, i) => (
16 |             
17 | {line.map((token, key) => ( 18 | 19 | ))} 20 |
21 | ))} 22 |
23 | )} 24 |
25 | ); 26 | } 27 | 28 | export default CommonHighlighter; 29 | -------------------------------------------------------------------------------- /.github/workflows/01_deploy_to_github_pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | repository_dispatch: 5 | types: [test-done] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v2 13 | 14 | - name: Set up Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: '20.x' 18 | 19 | - name: Install dependencies 20 | run: yarn install 21 | 22 | - name: Build project 23 | run: yarn build 24 | 25 | - name: Build example 26 | run: cd example && yarn install && yarn build 27 | 28 | - name: Copy 404.html 29 | run: cp example/build/index.html example/build/404.html 30 | 31 | - name: Deploy to GitHub Pages 32 | uses: peaceiris/actions-gh-pages@v3 33 | with: 34 | github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 35 | publish_dir: example/build 36 | -------------------------------------------------------------------------------- /.github/workflows/00_test.yml: -------------------------------------------------------------------------------- 1 | name: ✅ Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v3 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '20.x' 19 | registry-url: 'https://registry.npmjs.org' 20 | 21 | - name: Install dependencies 22 | run: yarn install 23 | 24 | - name: Build package 25 | run: yarn build 26 | 27 | - name: Test package 28 | run: yarn test 29 | 30 | - name: Dispatch test done event 31 | run: | 32 | curl -L \ 33 | -X POST \ 34 | -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ 35 | https://api.github.com/repos/almond-bongbong/react-simple-toasts/dispatches \ 36 | -d '{"event_type":"test-done"}' 37 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "dayjs": "^1.11.8", 14 | "prism-react-renderer": "^2.0.4", 15 | "react": "link:../node_modules/react", 16 | "react-dom": "link:../node_modules/react-dom", 17 | "react-router-dom": "^6.11.1", 18 | "react-simple-toasts": "link:.." 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^18.0.28", 22 | "@types/react-dom": "^18.0.11", 23 | "@typescript-eslint/eslint-plugin": "^5.57.1", 24 | "@typescript-eslint/parser": "^5.57.1", 25 | "@vitejs/plugin-react-swc": "^3.0.0", 26 | "eslint": "^8.38.0", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "eslint-plugin-react-refresh": "^0.3.4", 29 | "typescript": "^5.0.2", 30 | "vite": "^4.5.5" 31 | } 32 | } -------------------------------------------------------------------------------- /.github/workflows/02_publish_to_github.yml: -------------------------------------------------------------------------------- 1 | name: Publish Github Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | packages: write 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: '20.x' 21 | registry-url: 'https://npm.pkg.github.com' 22 | # Defaults to the user or organization that owns the workflow file 23 | scope: '@almond-bongbong' 24 | 25 | - name: Install dependencies 26 | run: yarn install 27 | 28 | - name: Build package 29 | run: yarn build 30 | 31 | - name: Install jq 32 | run: sudo apt-get install -y jq 33 | 34 | - name: Update package.json for GitHub Packages 35 | run: | 36 | jq '.name = "@almond-bongbong/react-simple-toasts"' package.json > package.temp.json 37 | mv package.temp.json package.json 38 | 39 | - name: Publish to GitHub Packages 40 | run: npm publish 41 | env: 42 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | -------------------------------------------------------------------------------- /example/src/util/debounce.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 2 | export type Procedure = (...args: any[]) => void; 3 | 4 | export type Options = { 5 | isImmediate: boolean; 6 | }; 7 | 8 | export interface DebouncedFunction { 9 | (this: ThisParameterType, ...args: Parameters): void; 10 | cancel: () => void; 11 | } 12 | 13 | export function debounce( 14 | func: F, 15 | waitMilliseconds = 50, 16 | options: Options = { 17 | isImmediate: false, 18 | }, 19 | ): DebouncedFunction { 20 | let timeoutId: ReturnType | undefined; 21 | 22 | const debouncedFunction = function ( 23 | this: ThisParameterType, 24 | ...args: Parameters 25 | ) { 26 | const doLater = function () { 27 | timeoutId = undefined; 28 | if (!options.isImmediate) { 29 | func(...args); 30 | } 31 | }; 32 | 33 | const shouldCallNow = options.isImmediate && timeoutId === undefined; 34 | 35 | if (timeoutId !== undefined) { 36 | clearTimeout(timeoutId); 37 | } 38 | 39 | timeoutId = setTimeout(doLater, waitMilliseconds); 40 | 41 | if (shouldCallNow) { 42 | func(...args); 43 | } 44 | }; 45 | 46 | debouncedFunction.cancel = function () { 47 | if (timeoutId !== undefined) { 48 | clearTimeout(timeoutId); 49 | } 50 | }; 51 | 52 | return debouncedFunction; 53 | } 54 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 4 | import typescript from 'rollup-plugin-typescript2'; 5 | import peerDepsExternal from 'rollup-plugin-peer-deps-external'; 6 | import eslint from '@rollup/plugin-eslint'; 7 | import json from '@rollup/plugin-json'; 8 | 9 | import fs from 'node:fs'; 10 | import { dirname, join } from 'node:path'; 11 | import { fileURLToPath } from 'node:url'; 12 | 13 | const configPath = join(dirname(fileURLToPath(import.meta.url)), './package.json'); 14 | const pkg = JSON.parse(fs.readFileSync(configPath, 'utf8')); 15 | 16 | const extensions = ['js', 'jsx', 'ts', 'tsx', 'mjs']; 17 | 18 | export default { 19 | input: './src/index.tsx', 20 | output: [ 21 | { 22 | file: pkg.main, 23 | format: 'cjs', 24 | sourcemap: true, 25 | }, 26 | { 27 | file: pkg.module, 28 | format: 'esm', 29 | sourcemap: true, 30 | }, 31 | ], 32 | plugins: [ 33 | eslint({ 34 | include: ['src/**/*.ts', 'src/**/*.tsx'], 35 | }), 36 | peerDepsExternal(), 37 | nodeResolve({ extensions }), 38 | typescript({ 39 | clean: true, 40 | tsconfig: './tsconfig.json', 41 | }), 42 | babel({ 43 | babelHelpers: 'bundled', 44 | exclude: 'node_modules/**', 45 | extensions, 46 | include: ['src/**/*'], 47 | }), 48 | commonjs({ include: 'node_modules/**' }), 49 | json(), 50 | ], 51 | }; 52 | -------------------------------------------------------------------------------- /src/style.scss: -------------------------------------------------------------------------------- 1 | #toast__container { 2 | * { 3 | box-sizing: border-box; 4 | } 5 | } 6 | 7 | .toast { 8 | &__message { 9 | opacity: 0; 10 | position: fixed; 11 | z-index: 1000; 12 | width: max-content; 13 | max-width: 90%; 14 | transition: opacity 0.3s, transform 0.3s; 15 | 16 | &--top-center, 17 | &--bottom-center, 18 | &--center { 19 | left: 50%; 20 | } 21 | 22 | &--enter-active, 23 | &--appear-active { 24 | opacity: 1; 25 | } 26 | 27 | &--exit-active { 28 | opacity: 0; 29 | } 30 | 31 | &--loading { 32 | .toast__spinner-wrap { 33 | visibility: visible; 34 | opacity: 1; 35 | width: 1.1em; 36 | margin-right: 12px; 37 | } 38 | } 39 | } 40 | 41 | &__content { 42 | display: flex; 43 | align-items: center; 44 | justify-content: center; 45 | 46 | &--clickable { 47 | cursor: pointer; 48 | } 49 | } 50 | 51 | &__theme-content { 52 | transition: all 0.2s; 53 | } 54 | 55 | &__spinner-wrap { 56 | display: inline-block; 57 | visibility: hidden; 58 | opacity: 0; 59 | width: 0; 60 | margin-right: 0; 61 | } 62 | 63 | &__spinner { 64 | display: block; 65 | overflow: hidden; 66 | width: 1.1em; 67 | height: 1.1em; 68 | border: 2px solid rgba(255, 255, 255, 0.3); 69 | border-radius: 50%; 70 | border-top-color: #fff; 71 | text-indent: -99999px; 72 | animation: toast_spinner-spin 1s ease-in-out infinite; 73 | } 74 | } 75 | 76 | @keyframes toast_spinner-spin { 77 | to { 78 | transform: rotate(360deg); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /example/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter, RouterProvider } from 'react-router-dom'; 2 | import GettingStarted from './page/getting-started'; 3 | import Root from './root'; 4 | import Home from './page/home'; 5 | import Api from './page/api'; 6 | import Example from './page/example'; 7 | import Theme from './page/theme'; 8 | import ChangeLog from './page/change-log'; 9 | import Preview from './page/preview'; 10 | import Section from './page/preview/section'; 11 | import Interaction from "./page/preview/interaction"; 12 | 13 | const router = createBrowserRouter( 14 | [ 15 | { 16 | path: '/', 17 | element: , 18 | children: [ 19 | { 20 | path: '/', 21 | element: , 22 | }, 23 | { 24 | path: '/getting-started', 25 | element: , 26 | }, 27 | { 28 | path: '/api', 29 | element: , 30 | }, 31 | { 32 | path: '/example', 33 | element: , 34 | }, 35 | { 36 | path: '/theme', 37 | element: , 38 | }, 39 | { 40 | path: '/change-log', 41 | element: , 42 | }, 43 | ], 44 | }, 45 | { 46 | path: '/preview', 47 | element: , 48 | }, 49 | { 50 | path: '/preview/section', 51 | element:
, 52 | }, 53 | { 54 | path: '/preview/interaction', 55 | element: , 56 | }, 57 | ], 58 | { 59 | basename: import.meta.env.DEV ? undefined : '/react-simple-toasts', 60 | }, 61 | ); 62 | 63 | function App() { 64 | return ; 65 | } 66 | 67 | export default App; 68 | -------------------------------------------------------------------------------- /src/type/common.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement, ReactNode, SyntheticEvent } from 'react'; 2 | import { Themes, ToastPosition as Position } from '../lib/constants'; 3 | import { ToastMessageProps } from '../component/toast-message'; 4 | 5 | export type ToastPosition = (typeof Position)[keyof typeof Position]; 6 | 7 | export type Theme = (typeof Themes)[keyof typeof Themes]; 8 | 9 | export type ToastClickHandler = (e: SyntheticEvent) => void | Promise; 10 | 11 | export interface ToastOptions { 12 | duration?: number | null; 13 | className?: string; 14 | clickable?: boolean; 15 | clickClosable?: boolean; 16 | position?: ToastPosition; 17 | offsetX?: number; 18 | offsetY?: number; 19 | gap?: number; 20 | maxVisibleToasts?: number | null; 21 | isReversedOrder?: boolean; 22 | render?: ((message: ReactNode) => ReactNode) | null; 23 | theme?: Theme | string | null; 24 | zIndex?: number; 25 | loading?: boolean | Promise; 26 | loadingText?: ReactNode; 27 | onClick?: ToastClickHandler; 28 | onClose?: () => void; 29 | onCloseStart?: () => void; 30 | } 31 | 32 | export type ToastEnterEvent = { target: HTMLDivElement; width: number; height: number }; 33 | 34 | export type ToastUpdateOptions = { 35 | message?: ReactNode; 36 | duration?: number; 37 | loading?: boolean; 38 | theme?: Theme; 39 | }; 40 | export type ToastUpdateArgs = 41 | | [message: ReactNode, duration?: ToastOptions['duration']] 42 | | [options: ToastUpdateOptions]; 43 | 44 | export interface Toast { 45 | close: () => void; 46 | updateDuration: (duration?: ToastOptions['duration']) => void; 47 | update: (...args: ToastUpdateArgs) => void; 48 | } 49 | 50 | export interface ToastComponent { 51 | id: number; 52 | message: ReactNode; 53 | position: ToastPosition; 54 | component: ReactElement; 55 | isExit?: boolean; 56 | width?: number; 57 | height?: number; 58 | gap: number; 59 | startCloseTimer: (duration?: number, callback?: () => void) => void; 60 | } 61 | -------------------------------------------------------------------------------- /example/src/page/getting-started/getting-started.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CommonHighlighter from '../../component/common-highlighter'; 3 | import styles from './getting-started.module.css'; 4 | 5 | function GettingStarted() { 6 | return ( 7 |
8 |
9 |

📦 Installation

10 |

To get started with React Simple Toasts, install the package using npm or yarn:

11 |
12 |
13 | {`// with npm 14 | npm install react-simple-toasts 15 | 16 | // with yarn 17 | yarn add react-simple-toasts`} 18 |
19 |
20 |
21 |

🛠 Usage

22 |

23 | Here is a simple example of how to use React Simple Toasts. 24 |
25 | As of version 4.0.0, a theme must be explicitly imported and set as a configuration 26 | option. Without a specified theme, no styles will be applied to the toast message. 27 |

28 |
29 |
30 | 31 | {`import React from 'react'; 32 | import toast, { toastConfig } from 'react-simple-toasts'; 33 | import 'react-simple-toasts/dist/style.css'; 34 | import 'react-simple-toasts/dist/theme/dark.css'; // import the desired theme 35 | 36 | // specify the theme in toastConfig 37 | toastConfig({ 38 | theme: 'dark', 39 | }); 40 | 41 | function App() { 42 | return ( 43 | 46 | ); 47 | } 48 | 49 | export default App;`} 50 | 51 |
52 |
53 |
54 |

🌟 Benefits of using React Simple Toasts:

55 |
    56 |
  • No need for Provider or any wrapper components.
  • 57 |
  • Simple theme configuration with built-in styles.
  • 58 |
59 |
60 |
61 | ); 62 | } 63 | 64 | export default GettingStarted; 65 | -------------------------------------------------------------------------------- /src/component/toast-container.tsx: -------------------------------------------------------------------------------- 1 | import React, { cloneElement, Fragment } from 'react'; 2 | import { reverse } from '../lib/utils'; 3 | import { ToastComponent, ToastEnterEvent } from '../type/common'; 4 | 5 | export interface ToastContainerProps { 6 | toastComponentList: ToastComponent[]; 7 | onToastEnter: () => void; 8 | } 9 | 10 | function ToastContainer(props: ToastContainerProps) { 11 | const { toastComponentList, onToastEnter } = props; 12 | 13 | const handleToastEnter = (t: ToastComponent, e: ToastEnterEvent) => { 14 | toastComponentList.forEach((toast) => { 15 | if (toast.id !== t.id) return; 16 | toast.startCloseTimer(); 17 | toast.height = e.height; 18 | }); 19 | 20 | onToastEnter(); 21 | }; 22 | 23 | return ( 24 | <> 25 | {toastComponentList.map((t) => { 26 | const toastComponents = t.position.includes('top') 27 | ? reverse(toastComponentList) 28 | : toastComponentList; 29 | 30 | const currentIndex = toastComponents.findIndex((toast) => toast.id === t.id); 31 | const bottomToasts = toastComponents 32 | .slice(currentIndex + 1) 33 | .filter((toast) => toast.position === t.position && !toast.isExit); 34 | 35 | const bottomToastsHeight = bottomToasts.reduce((acc, toast) => { 36 | return acc + (toast.height ?? 0) + t.gap; 37 | }, 0); 38 | 39 | const deltaOffsetX = 40 | t.position.includes('left') || t.position.includes('right') ? '0%' : '-50%'; 41 | const offsetYAlpha = t.position.includes('top') ? 1 : -1; 42 | const baseOffsetY = bottomToastsHeight * offsetYAlpha; 43 | const deltaOffsetY = 44 | t.position === 'center' ? `calc(-50% - ${baseOffsetY * -1}px)` : `${baseOffsetY}px`; 45 | 46 | return ( 47 | 48 | {cloneElement(t.component, { 49 | isExit: t.isExit, 50 | deltaOffsetX, 51 | deltaOffsetY, 52 | _onEnter: (event: ToastEnterEvent) => handleToastEnter(t, event), 53 | })} 54 | 55 | ); 56 | })} 57 | 58 | ); 59 | } 60 | 61 | export default ToastContainer; 62 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Sonsie+One&display=swap'); 2 | 3 | :root { 4 | --basic-font-size: 17px; 5 | } 6 | 7 | * { 8 | box-sizing: border-box; 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | html, 14 | body { 15 | height: 100%; 16 | } 17 | 18 | #root { 19 | height: 100%; 20 | } 21 | 22 | body { 23 | margin: 0; 24 | padding: 0; 25 | font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 26 | Arial, sans-serif; 27 | color: #292f36; 28 | font-size: var(--basic-font-size); 29 | line-height: 1.5; 30 | } 31 | 32 | h1 { 33 | font-family: 'Sonsie One', sans-serif; 34 | } 35 | 36 | a { 37 | color: inherit; 38 | text-decoration: none; 39 | } 40 | 41 | ul, 42 | ol { 43 | list-style: none; 44 | } 45 | 46 | .desc { 47 | margin-bottom: 80px; 48 | } 49 | 50 | h2 { 51 | margin-bottom: 20px; 52 | font-size: 30px; 53 | } 54 | 55 | h3 { 56 | margin-bottom: 10px; 57 | font-size: 22px; 58 | } 59 | 60 | h4 { 61 | margin-bottom: 10px; 62 | font-size: 18px; 63 | } 64 | 65 | p { 66 | font-size: var(--basic-font-size); 67 | } 68 | 69 | p a { 70 | color: #3498db; 71 | text-decoration: underline; 72 | } 73 | 74 | section + section { 75 | margin-top: 70px; 76 | } 77 | 78 | .example-button + .example-button { 79 | margin-left: 10px; 80 | } 81 | 82 | .playground { 83 | margin-bottom: 20px; 84 | } 85 | 86 | .example-area + .example-area { 87 | margin-top: 40px; 88 | } 89 | 90 | .my-toast { 91 | background-color: rgba(255, 107, 129, 0.9); 92 | padding: 10px 20px; 93 | color: #fff; 94 | border-radius: 3px; 95 | } 96 | 97 | hr { 98 | margin: 15px 0; 99 | border-top: 1px dashed #fafafa; 100 | } 101 | 102 | table { 103 | width: 100%; 104 | border-collapse: collapse; 105 | border-spacing: 0; 106 | border: 1px solid #ddd; 107 | } 108 | 109 | table th { 110 | background-color: #fafafa; 111 | text-align: left; 112 | } 113 | 114 | table th, 115 | table td { 116 | padding: 10px; 117 | border: 1px solid #ddd; 118 | } 119 | 120 | code { 121 | padding: 2px 4px; 122 | background-color: #f1f1f1; 123 | border-radius: 2px; 124 | font-family: 'Source Code Pro', monospace; 125 | color: #ff6b81; 126 | font-weight: 700; 127 | } 128 | 129 | select { 130 | height: 42px; 131 | padding: 5px 10px; 132 | border-radius: 3px; 133 | border: 1px solid #ccc; 134 | font-size: 16px; 135 | color: #555; 136 | } 137 | 138 | @media screen and (max-width: 1200px) { 139 | :root { 140 | --basic-font-size: 15px; 141 | } 142 | 143 | h2 { 144 | font-size: 24px; 145 | } 146 | 147 | h3 { 148 | font-size: 18px; 149 | } 150 | 151 | table { 152 | width: 600px; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /example/src/page/home/home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './home.module.css'; 3 | import toast from 'react-simple-toasts'; 4 | import Button from '../../component/button'; 5 | 6 | function Home() { 7 | return ( 8 |
9 |
10 |

🔍 Overview

11 |

12 | React Simple Toasts is a lightweight, easy-to-use library for creating toast notifications 13 | in your React applications. 14 |

15 |
16 | 17 |
18 | 19 |
20 |

🔑 Key Features

21 |
    22 |
  • 23 | Ease of use: With a simple installation process and an intuitive API, you can get 24 | started with the library in no time. 25 |
  • 26 |
  • 27 | Highly customizable: You can control various aspects of your toast messages, from 28 | their appearance and duration to their behavior upon user interaction. 29 |
  • 30 |
  • 31 | Custom rendering: The library supports custom rendering, allowing you to tailor 32 | the look of your toast messages to match your application's branding. 33 |
  • 34 |
  • 35 | Positioning: The library allows you to position your toasts at any corner or 36 | center of the viewport, offering a high level of control over where your messages 37 | appear. 38 |
  • 39 |
  • 40 | Browser compatibility: The library includes utility functions to ensure that it 41 | works seamlessly across different browsers. 42 |
  • 43 |
  • 44 | Interactive: The library allows toasts to be clickable and to close on click if 45 | desired, enabling user interaction. 46 |
  • 47 |
  • 48 | Multiple toasts management: It provides functionality to manage multiple toasts 49 | by controlling the maximum number of visible toasts at a time. 50 |
  • 51 |
52 |
53 | 54 |
55 |

💖 Support Us

56 |

57 | If you find this library useful, consider giving us a star on{' '} 58 | 63 | GitHub 64 | 65 | ! Your support is greatly appreciated and it helps the project grow. 66 |

67 |
68 | 69 |
70 |

⚖️ License

71 |

This project is licensed under the terms of the MIT license.

72 |
73 |
74 | ); 75 | } 76 | 77 | export default Home; 78 | -------------------------------------------------------------------------------- /src/lib/react-render.ts: -------------------------------------------------------------------------------- 1 | import type * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as ReactDOMClient from 'react-dom/client'; 4 | import type { Root } from 'react-dom/client'; 5 | 6 | // Let compiler not to search module usage 7 | const fullClone = { 8 | ...ReactDOM, 9 | ...ReactDOMClient, 10 | } as typeof ReactDOM & { 11 | __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?: { 12 | usingClientEntryPoint?: boolean; 13 | }; 14 | createRoot?: CreateRoot; 15 | render?: (node: React.ReactElement, container: ContainerType) => void; 16 | unmountComponentAtNode?: (container: ContainerType) => boolean; 17 | }; 18 | 19 | type CreateRoot = (container: ContainerType) => Root; 20 | 21 | const { version, render: reactRender, unmountComponentAtNode } = fullClone; 22 | 23 | let createRoot: CreateRoot; 24 | try { 25 | const mainVersion = Number((version || '').split('.')[0]); 26 | if (mainVersion >= 18 && fullClone.createRoot) { 27 | createRoot = fullClone.createRoot; 28 | } 29 | } catch (e) { 30 | // Do nothing; 31 | } 32 | 33 | function toggleWarning(skip: boolean) { 34 | const { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } = fullClone; 35 | 36 | if ( 37 | __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED && 38 | typeof __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED === 'object' 39 | ) { 40 | __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = skip; 41 | } 42 | } 43 | 44 | const MARK = '__rc_react_root__'; 45 | 46 | // ========================== Render ========================== 47 | type ContainerType = (Element | DocumentFragment) & { 48 | [MARK]?: Root; 49 | }; 50 | 51 | function modernRender(node: React.ReactNode, container: ContainerType) { 52 | toggleWarning(true); 53 | const root = container[MARK] || createRoot(container); 54 | toggleWarning(false); 55 | 56 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 57 | root.render(node as any); 58 | 59 | container[MARK] = root; 60 | } 61 | 62 | function legacyRender(node: React.ReactElement, container: ContainerType) { 63 | reactRender?.(node, container); 64 | } 65 | 66 | /** @private Test usage. Not work in prod */ 67 | export function _r(node: React.ReactElement, container: ContainerType) { 68 | if (process.env.NODE_ENV !== 'production') { 69 | return legacyRender(node, container); 70 | } 71 | } 72 | 73 | export function render(node: React.ReactElement, container: ContainerType) { 74 | if (createRoot != null) { 75 | modernRender(node, container); 76 | return; 77 | } 78 | 79 | legacyRender(node, container); 80 | } 81 | 82 | // ========================= Unmount ========================== 83 | async function modernUnmount(container: ContainerType) { 84 | // Delay to unmount to avoid React 18 sync warning 85 | return Promise.resolve().then(() => { 86 | container[MARK]?.unmount(); 87 | 88 | delete container[MARK]; 89 | }); 90 | } 91 | 92 | function legacyUnmount(container: ContainerType) { 93 | unmountComponentAtNode?.(container); 94 | } 95 | 96 | /** @private Test usage. Not work in prod */ 97 | export function _u(container: ContainerType) { 98 | if (process.env.NODE_ENV !== 'production') { 99 | return legacyUnmount(container); 100 | } 101 | } 102 | 103 | export async function unmount(container: ContainerType) { 104 | if (createRoot !== undefined) { 105 | // Delay to unmount to avoid React 18 sync warning 106 | return modernUnmount(container); 107 | } 108 | 109 | legacyUnmount(container); 110 | } 111 | -------------------------------------------------------------------------------- /example/src/page/preview/preview.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation, useNavigate } from 'react-router-dom'; 2 | import Button from '../../component/button/button'; 3 | import styles from './preview.module.css'; 4 | import { useLayoutEffect, useRef, useState } from 'react'; 5 | 6 | function Preview() { 7 | const navigate = useNavigate(); 8 | const { search } = useLocation(); 9 | const category = new URLSearchParams(search).get('category') || 'standard'; 10 | const containerRef = useRef(null); 11 | const [containerSize, setContainerSize] = useState([0, 0]); 12 | 13 | useLayoutEffect(() => { 14 | const container = containerRef.current; 15 | if (!container) return; 16 | 17 | setContainerSize([container.offsetWidth, container.offsetHeight]); 18 | }, []); 19 | 20 | return ( 21 |
22 |
23 | 24 | 25 |
26 | 27 |
28 |
29 | {containerSize[0]} x {containerSize[1]} 30 |
31 | {category === 'standard' && ( 32 | <> 33 |
34 |