├── .npmignore ├── setupTests.ts ├── .gitignore ├── babel.config.json ├── globals.d.ts ├── jest.config.js ├── .prettierrc ├── tsconfig.json ├── src ├── index.tsx ├── Spinner.tsx ├── ProgressBar.tsx ├── AlertContainer.tsx ├── Matrix.tsx ├── Icon.tsx ├── animations.ts ├── AlertInput.tsx ├── __tests__ │ └── components.jsx ├── helpers.ts ├── Poptart.tsx ├── Provider.tsx ├── constants.ts ├── types.ts └── Alert.tsx ├── .github └── workflows │ └── alltests.yml ├── CHANGELOG.md ├── rollup.config.mjs ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules -------------------------------------------------------------------------------- /setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | pnpm-lock.yaml 4 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ] 6 | } -------------------------------------------------------------------------------- /globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css' { 2 | const classes: { [key: string]: string }; 3 | export default classes; 4 | } 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: ['/setupTests.ts'], 3 | testEnvironment: 'jsdom', 4 | moduleNameMapper: { 5 | '^dist/(.*)$': '/dist/$1', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "useTabs": true, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "quoteProps": "as-needed" 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "lib": [ 6 | "dom", 7 | "ES2015", 8 | ], 9 | "declaration": true, 10 | "declarationDir": "dist/types", 11 | "outDir": "dist", 12 | "jsx": "react", 13 | "strict": true, 14 | "moduleResolution": "node", 15 | "esModuleInterop": true, 16 | "skipLibCheck": true 17 | }, 18 | "include": [ 19 | "src" 20 | ] 21 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export { PoptartProvider, usePoptart } from './Provider'; 2 | export { defaultConfig as defaultPoptartConfig } from './constants'; 3 | export { E_PoptartAlign, E_PoptartAnimation, E_PoptartStyle, E_PoptartType, E_AlertInputType } from './types'; 4 | export type { 5 | I_PoptartProviderProps, 6 | I_PoptartConfig, 7 | I_PoptartPromise, 8 | I_PoptartProps, 9 | T_PoptartType, 10 | T_PoptartColors, 11 | I_PoptartUserConfig, 12 | I_PoptartItem, 13 | I_PoptartProgressBarProps, 14 | I_PoptartContext, 15 | T_PoptartAlign, 16 | T_PoptartAnimation, 17 | I_AlertButton, 18 | I_AlertProps, 19 | I_AlertConfig, 20 | I_AlertInput, 21 | T_AlertInputType, 22 | T_PoptartStyle, 23 | } from './types'; 24 | -------------------------------------------------------------------------------- /.github/workflows/alltests.yml: -------------------------------------------------------------------------------- 1 | name: All Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 10 15 | 16 | strategy: 17 | matrix: 18 | node-version: [20] 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm install --force 27 | - run: npm run build 28 | - run: npm run test 29 | -------------------------------------------------------------------------------- /src/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { I_PoptartSpinnerConfig } from './types'; 4 | 5 | interface Props extends I_PoptartSpinnerConfig { 6 | size: number; 7 | isInverted: boolean; 8 | } 9 | 10 | export default function Spinner(props: Props) { 11 | const { size, strokeWidth, baseColor, accentColor, animationDuration, isInverted } = props; 12 | 13 | return ( 14 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.0.8] - 2024-09-08 4 | 5 | Initial release 6 | 7 | ## [1.0.9] - 2024-09-09 8 | 9 | - Added feature: text input in alerts 10 | - Added watermark icon background to alerts 11 | - Tweaked default colors 12 | - Bux fix: alert type always says "info" 13 | - Bug fix: multiple injected style elements 14 | 15 | ## [1.1.0] - 2024-09-10 16 | 17 | - CSS bug fixes 18 | - Made animation CSS smoother 19 | - Tweaked alert CSS and sizing 20 | - Bug fix: text would bust out of alert box 21 | 22 | ## [1.1.1] - 2024-09-20 23 | 24 | - Bug fix: fixed alert zIndexes 25 | 26 | ## [1.1.2] - 2024-09-21 27 | 28 | - Added feature: promise poptarts that resolve to success or error poptarts 29 | 30 | ## [1.2.1] - 2024-09-21 31 | 32 | - Added feature: inverted poptarts (forground and background swapped) 33 | 34 | ## [1.2.2] - 2024-09-21 35 | 36 | - Bug fix: incorrect return type for poptart.push() 37 | 38 | ## [1.2.3] - 2024-09-22 39 | 40 | - Color tweaks, contrast adjustments 41 | 42 | ## [1.2.4] - 2024-10-03 43 | 44 | - Bug fix: fixed custom alert confirm and cancel button labels and colors 45 | 46 | ## [1.2.5] - 2025-02-21 47 | 48 | - Support for React 19 -------------------------------------------------------------------------------- /src/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import { getContrastColor } from './helpers'; 4 | 5 | import { I_PoptartConfig, I_PoptartProps } from './types'; 6 | 7 | interface ProgressBarProps { 8 | poptart: I_PoptartProps; 9 | height: number; 10 | backgroundColor: string; 11 | config: I_PoptartConfig; 12 | colorOverride?: string; 13 | } 14 | 15 | const ProgressBar: React.FC = ({ poptart, height, backgroundColor, config, colorOverride }) => { 16 | const color = 17 | colorOverride ?? 18 | getContrastColor({ 19 | backgroundColor, 20 | lightColor: config.progressBar.lightColor, 21 | darkColor: config.progressBar.darkColor, 22 | }); 23 | 24 | const [width, setWidth] = useState(100); 25 | 26 | const duration = poptart.duration || config.defaultDuration; 27 | 28 | useEffect(() => { 29 | setTimeout(() => { 30 | setWidth(0); 31 | }, 100); 32 | }, []); 33 | 34 | const dynamicStyles: React.CSSProperties = { 35 | position: 'absolute', 36 | bottom: 0, 37 | left: 0, 38 | height: `${height}px`, 39 | backgroundColor: color, 40 | width: `${width}%`, 41 | transition: `width ${duration}ms linear`, 42 | ...config.styleOverrides.progressBar, 43 | }; 44 | 45 | return
; 46 | }; 47 | 48 | export default ProgressBar; 49 | -------------------------------------------------------------------------------- /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 json from '@rollup/plugin-json'; 5 | import peerDepsExternal from 'rollup-plugin-peer-deps-external'; 6 | import { terser } from 'rollup-plugin-terser'; 7 | import babel from '@rollup/plugin-babel'; 8 | // Use import assertions for JSON 9 | import pkg from './package.json' assert { type: 'json' }; 10 | 11 | export default { 12 | input: 'src/index.tsx', 13 | output: [ 14 | { 15 | file: pkg.main, 16 | format: 'cjs', 17 | sourcemap: true, 18 | }, 19 | { 20 | file: pkg.module, 21 | format: 'esm', 22 | sourcemap: true, 23 | }, 24 | ], 25 | plugins: [ 26 | peerDepsExternal(), 27 | resolve(), 28 | commonjs(), 29 | typescript({ tsconfig: './tsconfig.json' }), 30 | json(), 31 | babel({ 32 | exclude: 'node_modules/**', 33 | presets: ['@babel/preset-env', '@babel/preset-react'], 34 | babelHelpers: 'bundled', 35 | }), 36 | terser(), // Minify the output 37 | ], 38 | external: ['react', 'react-dom'], 39 | onwarn: (warning, warn) => { 40 | // Suppress specific warnings or all warnings if desired 41 | if (warning.code === 'CIRCULAR_DEPENDENCY') { 42 | return; 43 | } 44 | // Pass through other warnings 45 | warn(warning); 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /src/AlertContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Alert from './Alert'; 3 | 4 | import { I_PoptartConfig, I_AlertProps } from './types'; 5 | 6 | interface Props { 7 | config: I_PoptartConfig; 8 | currentAlert: I_AlertProps | null; 9 | dismissAlert: () => void; 10 | } 11 | 12 | export default function AlertContainer(props: Props) { 13 | const { config, currentAlert, dismissAlert } = props; 14 | 15 | const [showModal, setShowModal] = useState(false); 16 | const [showAlert, setShowAlert] = useState(false); 17 | 18 | useEffect(() => { 19 | if (currentAlert) { 20 | setShowModal(true); 21 | 22 | setTimeout(() => { 23 | setShowAlert(true); 24 | }, 200); 25 | } else { 26 | setShowAlert(false); 27 | setShowModal(false); 28 | } 29 | }, [currentAlert]); 30 | 31 | const styles: React.CSSProperties = { 32 | position: 'fixed', 33 | inset: 0, 34 | pointerEvents: 'none', 35 | display: 'flex', 36 | zIndex: config.zIndex + 1, 37 | ...config.styleOverrides.container, 38 | }; 39 | 40 | const modalStyles: React.CSSProperties = { 41 | pointerEvents: 'auto', 42 | position: 'absolute', 43 | inset: 0, 44 | display: 'flex', 45 | justifyContent: 'center', 46 | alignItems: 'center', 47 | backgroundColor: 'rgba(0, 0, 0, 0.5)', 48 | }; 49 | 50 | const handleModalClick = () => { 51 | const canDismiss = currentAlert?.allowClickOffDismissal 52 | ? currentAlert.allowClickOffDismissal 53 | : config.alerts.allowClickOffDismissal; 54 | if (canDismiss) { 55 | dismissAlert(); 56 | } 57 | }; 58 | 59 | return ( 60 |
61 | {showModal ? ( 62 |
63 | <> 64 | {showAlert && currentAlert ? ( 65 | 66 | ) : null} 67 | 68 |
69 | ) : null} 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-poptart", 3 | "description": "A simple and elegant notification toaster for React.js", 4 | "version": "1.2.5", 5 | "homepage": "https://react-poptart.vercel.app", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/designly1/react-poptart.git" 9 | }, 10 | "main": "dist/cjs/index.js", 11 | "module": "dist/esm/index.js", 12 | "types": "dist/esm/types/index.d.ts", 13 | "files": [ 14 | "dist" 15 | ], 16 | "scripts": { 17 | "build": "rollup -c", 18 | "dev": "rollup -c --watch & jest --watch", 19 | "push": "yalc push --no-installations --changed", 20 | "test": "jest", 21 | "test:watch": "jest --watch", 22 | "test:coverage": "jest --coverage" 23 | }, 24 | "watch": { 25 | "push": { 26 | "patterns": [ 27 | "dist/**/*" 28 | ], 29 | "extensions": "js,jsx,ts,tsx" 30 | } 31 | }, 32 | "keywords": [ 33 | "react", 34 | "notification", 35 | "toaster", 36 | "toast", 37 | "poptart", 38 | "alert" 39 | ], 40 | "author": { 41 | "name": "Jay Simons", 42 | "email": "jay@designly.biz", 43 | "url": "https://1337707.xyz" 44 | }, 45 | "license": "MIT", 46 | "peerDependencies": { 47 | "react": ">=18.3.1 <20", 48 | "react-dom": ">=18.3.1 <20" 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "^7.25.2", 52 | "@babel/preset-env": "^7.25.4", 53 | "@babel/preset-react": "^7.24.7", 54 | "@rollup/plugin-babel": "^6.0.4", 55 | "@rollup/plugin-commonjs": "^26.0.1", 56 | "@rollup/plugin-json": "^6.1.0", 57 | "@rollup/plugin-node-resolve": "^15.2.3", 58 | "@rollup/plugin-typescript": "^11.1.6", 59 | "@testing-library/jest-dom": "^6.5.0", 60 | "@testing-library/react": "^16.0.1", 61 | "@types/jest": "^29.5.12", 62 | "@types/react": "^18.3.5", 63 | "@types/react-dom": "^18.3.0", 64 | "jest": "^29.7.0", 65 | "jest-environment-jsdom": "^29.7.0", 66 | "npm-watch": "^0.13.0", 67 | "rollup": "^4.21.2", 68 | "rollup-plugin-peer-deps-external": "^2.2.4", 69 | "rollup-plugin-terser": "^7.0.2", 70 | "ts-jest": "^29.2.5", 71 | "tslib": "^2.7.0", 72 | "typescript": "^5.5.4" 73 | } 74 | } -------------------------------------------------------------------------------- /src/Matrix.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Poptart from './Poptart'; 3 | 4 | import { useAnimations } from './animations'; 5 | 6 | import { I_PoptartConfig, I_PoptartItem } from './types'; 7 | 8 | interface Props { 9 | config: I_PoptartConfig; 10 | poptarts: I_PoptartItem[]; 11 | dismiss: (id: string) => void; 12 | } 13 | 14 | const Matrix: React.FC = props => { 15 | const { config, poptarts, dismiss } = props; 16 | 17 | // Inject the animation keyframes into the DOM 18 | useAnimations(); 19 | 20 | const styleAdditions: React.CSSProperties = {}; 21 | 22 | // Align the matrix based on config 23 | switch (config.defaultAlign) { 24 | case 'tl': 25 | styleAdditions.justifyContent = 'flex-start'; 26 | styleAdditions.alignItems = 'flex-start'; 27 | break; 28 | case 'tc': 29 | styleAdditions.justifyContent = 'flex-start'; 30 | styleAdditions.alignItems = 'center'; 31 | break; 32 | case 'tr': 33 | styleAdditions.justifyContent = 'flex-start'; 34 | styleAdditions.alignItems = 'flex-end'; 35 | break; 36 | case 'bl': 37 | styleAdditions.justifyContent = 'flex-end'; 38 | styleAdditions.alignItems = 'flex-start'; 39 | break; 40 | case 'bc': 41 | styleAdditions.justifyContent = 'flex-end'; 42 | styleAdditions.alignItems = 'center'; 43 | break; 44 | case 'br': 45 | styleAdditions.justifyContent = 'flex-end'; 46 | styleAdditions.alignItems = 'flex-end'; 47 | break; 48 | default: 49 | styleAdditions.justifyContent = 'flex-end'; 50 | styleAdditions.alignItems = 'flex-end'; 51 | break; 52 | } 53 | 54 | // Main container styles 55 | const containerStyles: React.CSSProperties = { 56 | position: 'fixed', 57 | inset: 0, 58 | pointerEvents: 'none', 59 | display: 'flex', 60 | flexDirection: 'column', 61 | gap: '20px', 62 | padding: '20px', 63 | zIndex: config.zIndex + 2, 64 | ...styleAdditions, 65 | ...config.styleOverrides.container, 66 | }; 67 | 68 | return ( 69 |
70 | {poptarts.map((poptart) => ( 71 | 72 | ))} 73 |
74 | ); 75 | }; 76 | 77 | export default Matrix; 78 | -------------------------------------------------------------------------------- /src/Icon.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Font Awesome Free 6.6.0 by @fontawesome 3 | * https://fontawesome.com 4 | * License - https://fontawesome.com/license/free 5 | * Copyright 2024 Fonticons, Inc. 6 | */ 7 | 8 | import React from 'react'; 9 | 10 | import type { T_PoptartType } from './types'; 11 | 12 | interface Props { 13 | type: T_PoptartType; 14 | color: string; 15 | size: number; 16 | } 17 | 18 | export default function Icon(props: Props) { 19 | const { type, color, size } = props; 20 | 21 | switch (type) { 22 | case 'success': 23 | return ( 24 | 25 | 29 | 30 | ); 31 | case 'error': 32 | return ( 33 | 34 | 38 | 39 | ); 40 | case 'warning': 41 | return ( 42 | 43 | 47 | 48 | ); 49 | default: // info 50 | return ( 51 | 52 | 56 | 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/animations.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { injectStyle } from './helpers'; 3 | 4 | export function useAnimations() { 5 | const bounceInKeyframes = ` 6 | @keyframes bounceIn { 7 | 0% { 8 | opacity: 0; 9 | transform: translateY(-100%) scale(0.3); 10 | } 11 | 50% { 12 | opacity: 1; 13 | transform: translateY(0) scale(1.1); 14 | } 15 | 70% { 16 | transform: translateY(-10%) scale(0.95); 17 | } 18 | 100% { 19 | transform: translateY(0) scale(1); 20 | } 21 | } 22 | `; 23 | 24 | const fadeInKeyframes = ` 25 | @keyframes fadeIn { 26 | 0% { 27 | opacity: 0; 28 | } 29 | 100% { 30 | opacity: 1; 31 | } 32 | } 33 | `; 34 | 35 | const slideFromLeftKeyframes = ` 36 | @keyframes slideFromLeft { 37 | 0% { 38 | opacity: 0; 39 | transform: translateX(-100%); 40 | } 41 | 80% { 42 | transform: translateX(5%); 43 | } 44 | 100% { 45 | opacity: 1; 46 | transform: translateX(0); 47 | } 48 | } 49 | `; 50 | 51 | const slideFromRightKeyframes = ` 52 | @keyframes slideFromRight { 53 | 0% { 54 | opacity: 0; 55 | transform: translateX(100%); 56 | } 57 | 80% { 58 | transform: translateX(-5%); 59 | } 60 | 100% { 61 | opacity: 1; 62 | transform: translateX(0); 63 | } 64 | } 65 | `; 66 | 67 | const slideFromTopKeyframes = ` 68 | @keyframes slideFromTop { 69 | 0% { 70 | opacity: 0; 71 | transform: translateY(-100%); 72 | } 73 | 80% { 74 | transform: translateY(5%); 75 | } 76 | 100% { 77 | opacity: 1; 78 | transform: translateY(0); 79 | } 80 | } 81 | `; 82 | 83 | const slideFromBottomKeyframes = ` 84 | @keyframes slideFromBottom { 85 | 0% { 86 | opacity: 0; 87 | transform: translateY(100%); 88 | } 89 | 80% { 90 | transform: translateY(-5%); 91 | } 92 | 100% { 93 | opacity: 1; 94 | transform: translateY(0); 95 | } 96 | } 97 | `; 98 | 99 | const spin = ` 100 | @keyframes spin { 101 | 0% { 102 | transform: rotate(0deg); 103 | } 104 | 100% { 105 | transform: rotate(360deg); 106 | } 107 | } 108 | `; 109 | 110 | const beacon = ` 111 | @keyframes beacon { 112 | 0% { 113 | opacity: 0.5; 114 | } 115 | 50% { 116 | opacity: 1; 117 | } 118 | 100% { 119 | opacity: 0.5; 120 | } 121 | } 122 | `; 123 | 124 | useEffect(() => { 125 | const allStyles = 126 | bounceInKeyframes + 127 | fadeInKeyframes + 128 | slideFromLeftKeyframes + 129 | slideFromRightKeyframes + 130 | slideFromTopKeyframes + 131 | slideFromBottomKeyframes + 132 | spin + 133 | beacon; 134 | injectStyle('poptart-animations', allStyles); 135 | }, []); 136 | } 137 | -------------------------------------------------------------------------------- /src/AlertInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { injectStyle } from './helpers'; 3 | import { I_AlertInput, I_PoptartConfig, I_AlertProps } from './types'; 4 | 5 | interface Props { 6 | input: I_AlertInput; 7 | alert: I_AlertProps; 8 | config: I_PoptartConfig; 9 | value: string | undefined; 10 | setValue: React.Dispatch>; 11 | onConfirm: () => void; 12 | error: string | undefined; 13 | zIndex: number; 14 | } 15 | 16 | export default function AlertInput(props: Props) { 17 | const { input, config, value, setValue, error, alert, onConfirm, zIndex } = props; 18 | 19 | const { type, placeholder, required } = input; 20 | 21 | const backgroundColor = config.alerts.input.backgroundColor; 22 | const fontColor = config.alerts.input.fontColor; 23 | const borderRadius = config.alerts.input.borderRadius; 24 | const borderWidth = config.alerts.input.borderWidth; 25 | const paddingX = config.alerts.input.paddingX; 26 | const paddingY = config.alerts.input.paddingY; 27 | const maxWidth = config.alerts.input.maxWidth; 28 | const errorFeedbackColor = config.alerts.input.errorFeedbackColor; 29 | const alertBackgroundColor = alert.backgroundColor || config.alerts.defaultBackgroundColor; 30 | const placeholderColor = config.alerts.input.placeholderColor; 31 | 32 | useEffect(() => { 33 | injectStyle( 34 | 'poptart-input-styles', 35 | ` 36 | .poptart-native-input::placeholder { 37 | color: ${placeholderColor}; 38 | } 39 | `, 40 | ); 41 | }, [placeholderColor]); 42 | 43 | const styles: { [key: string]: React.CSSProperties } = { 44 | container: { 45 | width: '100%', 46 | display: 'flex', 47 | flexDirection: 'column' as 'column', 48 | alignItems: 'center', 49 | gap: '5px', 50 | zIndex, 51 | }, 52 | outer: { 53 | backgroundColor, 54 | borderRadius: `${borderRadius}px`, 55 | borderWidth: `${borderWidth}px`, 56 | padding: `${paddingY}px ${paddingX}px`, 57 | width: '100%', 58 | maxWidth, 59 | }, 60 | input: { 61 | backgroundColor: 'transparent', 62 | color: fontColor, 63 | border: 'none', 64 | outline: 'none', 65 | width: '100%', 66 | }, 67 | errorFeedback: { 68 | color: error ? errorFeedbackColor : alertBackgroundColor, 69 | fontSize: '0.8em', 70 | textAlign: 'center' as 'center', 71 | }, 72 | }; 73 | 74 | const handleChange = (e: React.ChangeEvent) => { 75 | setValue(e.target.value); 76 | }; 77 | 78 | const handleKeyDown = (e: React.KeyboardEvent) => { 79 | if (e.key === 'Enter') { 80 | onConfirm(); 81 | } 82 | }; 83 | 84 | return ( 85 |
86 |
87 | 97 |
98 | 99 | {error || '.'} 100 | 101 |
102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /src/__tests__/components.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent, waitFor } from '@testing-library/react'; 3 | import { PoptartProvider, usePoptart } from 'dist/esm/index'; 4 | 5 | // Mock helper functions 6 | jest.mock('../helpers', () => ({ 7 | generateSecureString: jest.fn().mockReturnValue('mock-id'), 8 | getContrastColor: jest.fn().mockReturnValue('#FFFFFF'), 9 | })); 10 | 11 | // Test Component to consume the PoptartContext 12 | const TestComponent = () => { 13 | const { push, dismiss, poptarts } = usePoptart(); 14 | 15 | const handlePush = () => { 16 | // Set custom duration to 1 second (1000 ms) for faster auto-dismiss 17 | push({ message: 'Test message', type: 'success', duration: 1000 }); 18 | }; 19 | 20 | const handleDismiss = id => { 21 | dismiss(id); 22 | }; 23 | 24 | return ( 25 |
26 | 27 |
    28 | {poptarts.map(poptart => ( 29 |
  • 30 | {poptart.props.message} 31 | 32 |
  • 33 | ))} 34 |
35 |
36 | ); 37 | }; 38 | 39 | describe('PoptartProvider', () => { 40 | it('renders the provider and can push a poptart', async () => { 41 | render( 42 | 43 | 44 | , 45 | ); 46 | 47 | // Click to push a poptart 48 | fireEvent.click(screen.getByText('Push Poptart')); 49 | 50 | // Assert that the poptart is pushed 51 | await waitFor(() => { 52 | const listItems = screen.queryAllByRole('listitem'); 53 | expect(listItems.length).toBeGreaterThan(0); // Check that list is populated 54 | }); 55 | 56 | // Use getAllByText to handle multiple matches 57 | const messages = screen.getAllByText('Test message'); 58 | expect(messages.length).toBeGreaterThan(0); // Ensure there are multiple messages 59 | }); 60 | 61 | it('dismisses a poptart', async () => { 62 | render( 63 | 64 | 65 | , 66 | ); 67 | 68 | // Push a poptart 69 | fireEvent.click(screen.getByText('Push Poptart')); 70 | 71 | // Ensure the poptart is rendered 72 | const messages = screen.getAllByText('Test message'); 73 | expect(messages.length).toBeGreaterThan(0); 74 | 75 | // Dismiss the poptart 76 | fireEvent.click(screen.getByText('Dismiss')); 77 | 78 | // Wait for the poptart to be removed 79 | await waitFor(() => { 80 | const listItems = screen.queryAllByRole('listitem'); 81 | expect(listItems.length).toEqual(0); // Assert that the list is empty 82 | }); 83 | }); 84 | 85 | it('correctly sets the duration and auto-dismisses', async () => { 86 | render( 87 | 88 | 89 | , 90 | ); 91 | 92 | // Push a poptart with a short duration of 1 second (1000ms) 93 | fireEvent.click(screen.getByText('Push Poptart')); 94 | 95 | // Verify poptart is rendered 96 | const messages = screen.getAllByText('Test message'); 97 | expect(messages.length).toBeGreaterThan(0); 98 | 99 | // Wait for the poptart to be auto-dismissed 100 | await waitFor( 101 | () => { 102 | const listItems = screen.queryAllByRole('listitem'); 103 | expect(listItems.length).toEqual(0); // Ensure the poptart is auto-dismissed 104 | }, 105 | { timeout: 2000 }, // Adjusted timeout to 2 seconds for the shorter duration 106 | ); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | export function getContrastColor({ 2 | backgroundColor, 3 | lightColor, 4 | darkColor, 5 | threshold = 0.38, 6 | }: { 7 | backgroundColor: string; 8 | lightColor: string; 9 | darkColor: string; 10 | threshold?: number; 11 | }): string { 12 | // Helper function to calculate luminance of a color 13 | const getLuminance = (r: number, g: number, b: number) => { 14 | const [red, green, blue] = [r, g, b].map(channel => { 15 | const c = channel / 255; 16 | return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); 17 | }); 18 | return 0.2126 * red + 0.7152 * green + 0.0722 * blue; 19 | }; 20 | 21 | // Helper function to convert hex color to RGB 22 | const hexToRgb = (hex: string) => { 23 | let normalizedHex = hex.replace('#', ''); 24 | if (normalizedHex.length === 3) { 25 | normalizedHex = normalizedHex 26 | .split('') 27 | .map(char => char + char) 28 | .join(''); 29 | } 30 | const bigint = parseInt(normalizedHex, 16); 31 | return { 32 | r: (bigint >> 16) & 255, 33 | g: (bigint >> 8) & 255, 34 | b: bigint & 255, 35 | }; 36 | }; 37 | 38 | // Convert hex color to RGB 39 | const { r, g, b } = hexToRgb(backgroundColor); 40 | 41 | // Calculate the luminance 42 | const luminance = getLuminance(r, g, b); 43 | 44 | // Return lightColor if the background is dark, and darkColor if the background is light 45 | return luminance > threshold ? darkColor : lightColor; 46 | } 47 | 48 | // Function to dynamically inject styles into the document 49 | export const injectStyle = (id: string, style: string) => { 50 | const existingStyleElement = document.getElementById(id); 51 | if (existingStyleElement) { 52 | existingStyleElement.innerHTML = style; 53 | return; 54 | } 55 | const styleElement = document.createElement('style'); 56 | styleElement.id = id; 57 | styleElement.innerHTML = style; 58 | document.head.appendChild(styleElement); 59 | }; 60 | 61 | export function generateSecureString(length: number): string { 62 | if (length <= 0) { 63 | throw new Error('Length must be a positive number.'); 64 | } 65 | 66 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 67 | const maxByte = 256 - (256 % chars.length); 68 | let secureString = ''; 69 | 70 | while (secureString.length < length) { 71 | const randomBytes = new Uint8Array(length); 72 | crypto.getRandomValues(randomBytes); 73 | 74 | for (let i = 0; i < randomBytes.length && secureString.length < length; i++) { 75 | const randomByte = randomBytes[i]; 76 | 77 | if (randomByte < maxByte) { 78 | secureString += chars[randomByte % chars.length]; 79 | } 80 | } 81 | } 82 | 83 | return secureString; 84 | } 85 | 86 | export function isObject(item: any): item is object { 87 | return item && typeof item === 'object' && !Array.isArray(item); 88 | } 89 | 90 | export function deepMerge(target: T, ...sources: T2[]): T { 91 | if (!sources.length) return target; 92 | 93 | const source = sources.shift(); 94 | 95 | if (source) { 96 | Object.keys(source).forEach(key => { 97 | const targetValue = (target as any)[key]; 98 | const sourceValue = (source as any)[key]; 99 | 100 | if (isObject(targetValue) && isObject(sourceValue)) { 101 | (target as any)[key] = deepMerge({ ...targetValue }, sourceValue); 102 | } else { 103 | (target as any)[key] = sourceValue; 104 | } 105 | }); 106 | } 107 | 108 | return deepMerge(target, ...sources); 109 | } 110 | -------------------------------------------------------------------------------- /src/Poptart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from './Icon'; 3 | import ProgressBar from './ProgressBar'; 4 | import Spinner from './Spinner'; 5 | 6 | import type { I_PoptartItem, I_PoptartConfig } from './types'; 7 | 8 | interface Props extends I_PoptartItem { 9 | config: I_PoptartConfig; 10 | dismiss: (id: string) => void; 11 | } 12 | 13 | const Poptart: React.FC = superProps => { 14 | const { props, id, foregroundColor, config, dismiss } = superProps; 15 | const { message, onClick } = props; 16 | 17 | const type = props.type || config.defaultType; 18 | const backgroundColor = config.colors[type]; 19 | const width = props.width || config.defaultWidth; 20 | const fontSize = config.fontSize; 21 | const paddingX = config.paddingX; 22 | const paddingY = config.paddingY; 23 | const progressBarHeight = config.progressBar.height; 24 | const duration = props.duration !== undefined ? props.duration : config.defaultDuration; 25 | const hasDuration = duration > 0; 26 | const animation = props.animation || config.defaultAnimation; 27 | const animationDuration = props.animationDuration || config.defaultAnimationDuration; 28 | const isLoading = props.type === 'loading'; 29 | const useProgressBar = hasDuration && !isLoading; 30 | 31 | const defaultPoptartStyle = config.defaultPoptartStyle === 'default' ? 'filled' : config.defaultPoptartStyle; 32 | let poptartStyle = defaultPoptartStyle; 33 | if (props.poptartStyle) { 34 | poptartStyle = props.poptartStyle === 'default' ? defaultPoptartStyle : props.poptartStyle; 35 | } 36 | const isInverted = poptartStyle === 'inverted'; 37 | 38 | // Dynamic styles 39 | const dynamicStyles: React.CSSProperties = { 40 | backgroundColor: isInverted ? config.colors.invertedBackground : backgroundColor, 41 | color: isInverted ? backgroundColor : foregroundColor, 42 | width, 43 | maxWidth: '100%', 44 | paddingTop: `${paddingY}px`, 45 | paddingLeft: `${paddingX}px`, 46 | paddingRight: `${paddingX}px`, 47 | paddingBottom: `${paddingY + (hasDuration ? progressBarHeight : 0)}px`, 48 | borderRadius: '10px', 49 | overflow: 'hidden', 50 | cursor: 'pointer', 51 | position: 'relative', 52 | pointerEvents: 'auto', 53 | fontSize, 54 | boxShadow: '0 0 10px rgba(0, 0, 0, 0.2)', 55 | border: '1px solid rgba(0, 0, 0, 0.1)', 56 | animation: `${animation} ${animationDuration}s ease-out`, 57 | ...config.styleOverrides.poptart, 58 | }; 59 | 60 | const innerStyle: React.CSSProperties = { 61 | display: 'flex', 62 | alignItems: 'center', 63 | gap: '10px', 64 | }; 65 | 66 | const textStyle: React.CSSProperties = { 67 | wordBreak: 'break-word', 68 | hyphens: 'auto', 69 | overflowWrap: 'break-word', 70 | textShadow: isInverted ? `2px 2px 2px rgba(0, 0, 0, 0.1)` : `2px 2px 2px rgba(0, 0, 0, 0.3)`, 71 | animation: isLoading ? 'beacon 1s infinite' : 'none', 72 | }; 73 | 74 | const handleClick = () => { 75 | dismiss(id); 76 | onClick && onClick(); 77 | }; 78 | 79 | const iconSize = fontSize * config.iconSizeFactor; 80 | 81 | return ( 82 |
83 |
84 | {isLoading ? ( 85 | 86 | ) : ( 87 |
88 | 89 |
90 | )} 91 | 92 | {message} 93 | 94 |
95 | {useProgressBar ? ( 96 | 103 | ) : null} 104 |
105 | ); 106 | }; 107 | 108 | export default Poptart; 109 | -------------------------------------------------------------------------------- /src/Provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState } from 'react'; 2 | import Matrix from './Matrix'; 3 | import AlertContainer from './AlertContainer'; 4 | 5 | import { getContrastColor, generateSecureString, deepMerge } from './helpers'; 6 | 7 | import { defaultConfig } from './constants'; 8 | 9 | import type { 10 | I_PoptartProviderProps, 11 | I_PoptartContext, 12 | I_PoptartItem, 13 | I_PoptartProps, 14 | I_PoptartConfig, 15 | I_PoptartUserConfig, 16 | I_AlertProps, 17 | I_PoptartPromise, 18 | } from './types'; 19 | 20 | // Create the Poptart context 21 | const PoptartContext = createContext(undefined); 22 | 23 | export const PoptartProvider: React.FC = ({ children, config: userConfig }) => { 24 | // Merge the default options with the user options 25 | const config = deepMerge(defaultConfig, userConfig || {}); 26 | 27 | // State 28 | const [poptarts, setPoptarts] = useState([]); 29 | const [currentAlert, setCurrentAlert] = useState(null); 30 | 31 | // Methods 32 | const push = (props: I_PoptartProps): string => { 33 | const id = generateSecureString(64); 34 | const hasPromise = typeof props.promise !== typeof undefined; 35 | 36 | const type = hasPromise ? 'loading' : props.type || config.defaultType; 37 | props.type = type; 38 | 39 | const foregroundColor = getContrastColor({ 40 | backgroundColor: config.colors[type], 41 | lightColor: config.colors.textLight, 42 | darkColor: config.colors.textDark, 43 | }); 44 | 45 | let duration = config.defaultDuration; 46 | if (props.duration !== undefined) { 47 | duration = props.duration; 48 | } 49 | 50 | setPoptarts(prev => [ 51 | ...prev, 52 | { 53 | id, 54 | props, 55 | expires: new Date(Date.now() + duration), 56 | progress: duration > 0 ? 100 : 0, 57 | foregroundColor, 58 | }, 59 | ]); 60 | 61 | if (duration > 0 && !hasPromise) { 62 | setTimeout(() => { 63 | dismiss(id); 64 | }, duration); 65 | } 66 | 67 | // If the poptart has a promise, return the promise 68 | if (hasPromise) { 69 | const promise = props.promise as I_PoptartPromise; 70 | promise.promise 71 | .then(() => { 72 | push({ 73 | message: promise.successMessage, 74 | type: 'success', 75 | duration: config.defaultDuration, 76 | }); 77 | dismiss(id); 78 | }) 79 | .catch(() => { 80 | push({ 81 | message: promise.errorMessage, 82 | type: 'error', 83 | duration: config.defaultDuration, 84 | }); 85 | dismiss(id); 86 | }); 87 | } 88 | 89 | return id; 90 | }; 91 | 92 | const promise = ( 93 | message: string, 94 | promise: I_PoptartPromise, 95 | overrides: Partial | undefined = undefined, 96 | ): string => { 97 | const props: I_PoptartProps = { 98 | message, 99 | promise, 100 | ...overrides, 101 | }; 102 | return push(props); 103 | }; 104 | 105 | // Dismiss a poptart 106 | const dismiss = (id: string) => { 107 | setPoptarts(prev => prev.filter(poptart => poptart.id !== id)); 108 | }; 109 | 110 | const alert = (props: I_AlertProps) => { 111 | setCurrentAlert(props); 112 | }; 113 | 114 | const dismissAlert = () => { 115 | setCurrentAlert(null); 116 | }; 117 | 118 | return ( 119 | 131 | {children} 132 | 133 | 134 | 135 | ); 136 | }; 137 | 138 | export const usePoptart = (): I_PoptartContext => { 139 | const context = useContext(PoptartContext); 140 | if (!context) { 141 | throw new Error('usePoptart must be used within a PoptartProvider'); 142 | } 143 | return context; 144 | }; 145 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { I_PoptartConfig } from '.'; 2 | 3 | export const defaultConfig: I_PoptartConfig = { 4 | // Default colors for the Poptart notifications 5 | colors: { 6 | success: '#07bc0c', // Green color for success notifications 7 | error: '#e74c3c', // Red color for error notifications 8 | warning: '#f1c40f', // Yellow color for warning notifications 9 | info: '#3498db', // Blue color for info notifications 10 | loading: '#4b5155', // Gray color for loading notifications 11 | textLight: '#fcfcfc', // Light contrasting color for text 12 | textDark: '#171717', // Dark contrasting color for text 13 | invertedBackground: '#f6f6f6', // Background color for inverted notifications 14 | }, 15 | // Override styles for various Poptart components 16 | styleOverrides: { 17 | container: {}, // Custom styles for the Poptart container 18 | poptart: {}, // Custom styles for individual Poptart notifications 19 | progressBar: {}, // Custom styles for the progress bar 20 | alertContainer: {}, // Custom styles for the alert container 21 | alert: {}, // Custom styles for the alert itself 22 | }, 23 | // Default alignment of Poptarts (possible values: 'tl' - top-left, 'tc' - top-center, 'tr' - top-right, 'bl' 24 | // - bottom-left, 'bc' - bottom-center, 'br' - bottom-right) 25 | defaultPoptartStyle: 'default', // Default style of the notification (possible values: 'default', 'filled', 'inverted') 26 | defaultAlign: 'br', 27 | // Default type of notification (possible values: 'success', 'error', 'warning', 'info') 28 | defaultType: 'info', 29 | // Default duration for which the notification is displayed (in milliseconds) 30 | defaultDuration: 5000, 31 | // Default width of the notification poptart 32 | defaultWidth: '450px', 33 | // Default animation for the notification appearance (possible values: 'bounceIn', 'fadeIn', 'slideFromLeft', 34 | // 'slideFromRight', 'slideFromTop', 'slideFromBottom') 35 | defaultAnimation: 'bounceIn', 36 | // Default animation duration for notifications (in seconds) 37 | defaultAnimationDuration: 0.6, 38 | // Default font size used in the notification text 39 | fontSize: 16, 40 | // Factor to adjust the size of icons relative to the font size 41 | iconSizeFactor: 2.5, 42 | // Customization for the progress bar in the notification 43 | progressBar: { 44 | lightColor: '#D6D6D6', // Color for the light theme of the progress bar 45 | darkColor: '#454545', // Color for the dark theme of the progress bar 46 | height: 5, // Height of the progress bar 47 | }, 48 | // Minimum contrast threshold for readability (used in calculating appropriate text color contrasts) 49 | contrastThreshold: 0.32, 50 | // Padding around the Poptart notification (X-axis) 51 | paddingX: 20, 52 | // Padding around the Poptart notification (Y-axis) 53 | paddingY: 16, 54 | // The z-index to control stacking order of the notification 55 | zIndex: 10, 56 | 57 | // Default settings for alerts (confirm/cancel dialog boxes) 58 | alerts: { 59 | defaultWidth: '600px', // Default width of the alert 60 | paddingX: 30, // Padding (X-axis) inside the alert box 61 | paddingY: 26, // Padding (Y-axis) inside the alert box 62 | borderRadius: 10, // Border radius for rounded corners of the alert box 63 | defaultType: 'info', // Default type of alert (info, success, error, or warning) 64 | defaultBackgroundColor: '#ffffff', // Default background color of the alert 65 | defaultFontColor: '#000', // Default text color inside the alert 66 | defaultFontSize: 20, // Default font size for the alert text 67 | defaultTitleFontSize: 28, // Font size for the alert title 68 | iconSizeFactor: 2, // Factor to adjust the size of icons within alerts 69 | borderWidth: 3, // Border width around the alert box 70 | defaultConfirmButtonColor: '#2d2d2d', // Color of the confirm button 71 | defaultCancelButtonColor: '#6B6B6B', // Color of the cancel button 72 | defaultConfirmButtonLabel: 'Ok', // Label text for the confirm button 73 | defaultCancelButtonLabel: 'Cancel', // Label text for the cancel button 74 | defaultShowCancelButton: false, // Whether the cancel button is shown by default 75 | defaultShowConfirmButton: true, // Whether the confirm button is shown by default 76 | defaultAnimation: 'slideFromBottom', // Default animation for alert boxes 77 | defaultAnimationDuration: 0.25, // Default duration for alert box animations (in seconds) 78 | allowClickOffDismissal: true, // Whether clicking outside the alert dismisses it 79 | 80 | // Default input styles for alerts (if input is used) 81 | input: { 82 | backgroundColor: '#fcfcfcac', // Background color for input fields in alerts 83 | fontColor: '#000', // Text color for input fields 84 | borderRadius: 5, // Border radius for the input field 85 | borderWidth: 1, // Border width around the input field 86 | paddingX: 10, // Padding (X-axis) for the input field 87 | paddingY: 8, // Padding (Y-axis) for the input field 88 | maxWidth: '70%', // Maximum width of the input field 89 | errorFeedbackColor: '#d12c2c', // Color used to indicate errors in input validation 90 | placeholderColor: '#a0a0a0', // Placeholder text color 91 | }, 92 | }, 93 | // Default settings for the spinner component 94 | spinner: { 95 | strokeWidth: 7, // Thickness of the spinner ring 96 | baseColor: '#f3f3f3', // Background color of the spinner ring 97 | accentColor: '#bbbbbb', // Accent color of the spinner ring 98 | animationDuration: 1, // Duration of the spinner animation (in seconds) 99 | }, 100 | }; 101 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import type { ReactNode } from 'react'; 4 | 5 | // Context interface 6 | export interface I_PoptartContext { 7 | poptarts: I_PoptartItem[]; 8 | config: I_PoptartConfig; 9 | currentAlert: I_AlertProps | null; 10 | push: (props: I_PoptartProps) => string; 11 | promise: (message: string, promise: I_PoptartPromise, overrides?: Partial) => string; 12 | dismiss: (id: string) => void; 13 | alert: (props: I_AlertProps) => void; 14 | dismissAlert: () => void; 15 | } 16 | 17 | // Enumerations 18 | export enum E_PoptartType { 19 | success = 'Success', 20 | error = 'Error', 21 | warning = 'Warning', 22 | info = 'Info', 23 | loading = 'Loading', 24 | } 25 | 26 | export enum E_PoptartAlign { 27 | tl = 'Top Left', 28 | tc = 'Top Center', 29 | tr = 'Top Right', 30 | bl = 'Bottom Left', 31 | bc = 'Bottom Center', 32 | br = 'Bottom Right', 33 | } 34 | 35 | export enum E_PoptartStyle { 36 | default = 'Default', 37 | filled = 'Filled', 38 | inverted = 'Inverted', 39 | } 40 | 41 | export enum E_PoptartAnimation { 42 | bounceIn = 'Bounce In', 43 | fadeIn = 'Fade In', 44 | slideFromLeft = 'Slide From Left', 45 | slideFromRight = 'Slide From Right', 46 | slideFromTop = 'Slide From Top', 47 | slideFromBottom = 'Slide From Bottom', 48 | } 49 | 50 | // Poptart types 51 | export type T_PoptartType = keyof typeof E_PoptartType; 52 | export type T_PoptartAlign = keyof typeof E_PoptartAlign; 53 | export type T_PoptartStyle = keyof typeof E_PoptartStyle; 54 | export type T_PoptartAnimation = keyof typeof E_PoptartAnimation; 55 | 56 | export interface I_PoptartPromise { 57 | promise: Promise; 58 | successMessage: string; 59 | errorMessage: string; 60 | } 61 | 62 | // Interface for user callable poptart.push() 63 | export interface I_PoptartProps { 64 | message: string; 65 | type?: T_PoptartType; 66 | poptartStyle?: T_PoptartStyle; 67 | duration?: number; 68 | width?: string; 69 | animation?: T_PoptartAnimation; 70 | animationDuration?: number; 71 | onClick?: () => void; 72 | promise?: I_PoptartPromise; 73 | } 74 | 75 | export interface I_PoptartItem { 76 | id: string; 77 | props: I_PoptartProps; 78 | expires: Date | null; 79 | foregroundColor: string; 80 | } 81 | 82 | // Poptart colors interface 83 | export type T_PoptartColors = { 84 | success: string; 85 | error: string; 86 | warning: string; 87 | info: string; 88 | textLight: string; 89 | textDark: string; 90 | invertedBackground: string; 91 | loading: string; 92 | }; 93 | 94 | // Interface for the progress bar 95 | export interface I_PoptartProgressBarProps { 96 | lightColor: string; 97 | darkColor: string; 98 | height: number; 99 | } 100 | 101 | export interface I_AlertConfig { 102 | defaultWidth: string; 103 | paddingX: number; 104 | paddingY: number; 105 | borderRadius: number; 106 | defaultType: T_PoptartType; 107 | defaultBackgroundColor: string; 108 | defaultFontColor: string; 109 | defaultFontSize: number; 110 | defaultTitleFontSize: number; 111 | iconSizeFactor: number; 112 | borderWidth: number; 113 | defaultConfirmButtonColor: string; 114 | defaultCancelButtonColor: string; 115 | defaultConfirmButtonLabel: string; 116 | defaultCancelButtonLabel: string; 117 | defaultShowCancelButton: boolean; 118 | defaultShowConfirmButton: boolean; 119 | defaultAnimation: T_PoptartAnimation; 120 | defaultAnimationDuration: number; 121 | allowClickOffDismissal: boolean; 122 | input: { 123 | backgroundColor: string; 124 | fontColor: string; 125 | borderRadius: number; 126 | borderWidth: number; 127 | paddingX: number; 128 | paddingY: number; 129 | maxWidth: string; 130 | errorFeedbackColor: string; 131 | placeholderColor: string; 132 | }; 133 | } 134 | 135 | // Spinner config interface 136 | export interface I_PoptartSpinnerConfig { 137 | strokeWidth: number; 138 | baseColor: string; 139 | accentColor: string; 140 | animationDuration: number; 141 | } 142 | 143 | // Poptart config interface 144 | export interface I_PoptartConfig { 145 | colors: T_PoptartColors; 146 | styleOverrides: { 147 | container: React.CSSProperties; 148 | poptart: React.CSSProperties; 149 | progressBar: React.CSSProperties; 150 | alertContainer: React.CSSProperties; 151 | alert: React.CSSProperties; 152 | }; 153 | defaultPoptartStyle: T_PoptartStyle; 154 | defaultAlign: T_PoptartAlign; 155 | defaultType: T_PoptartType; 156 | defaultDuration: number; 157 | defaultWidth: string; 158 | defaultAnimation: T_PoptartAnimation; 159 | defaultAnimationDuration: number; 160 | fontSize: number; 161 | iconSizeFactor: number; 162 | progressBar: I_PoptartProgressBarProps; 163 | contrastThreshold: number; 164 | paddingX: number; 165 | paddingY: number; 166 | zIndex: number; 167 | alerts: I_AlertConfig; 168 | spinner: I_PoptartSpinnerConfig; 169 | } 170 | 171 | // Popart user config interface 172 | export interface I_PoptartUserConfig { 173 | colors?: Partial; 174 | styleOverrides?: { 175 | container?: React.CSSProperties; 176 | poptart?: React.CSSProperties; 177 | progressBar?: React.CSSProperties; 178 | alertContainer?: React.CSSProperties; 179 | alert?: React.CSSProperties; 180 | }; 181 | defaultPoptartStyle?: T_PoptartStyle; 182 | defaultAlign?: T_PoptartAlign; 183 | defaultType?: T_PoptartType; 184 | defaultDuration?: number; 185 | defaultWidth?: string; 186 | defaultAnimation?: T_PoptartAnimation; 187 | defaultAnimationDuration?: number; 188 | fontSize?: number; 189 | iconSizeFactor?: number; 190 | progressBar?: Partial; 191 | contrastThreshold?: number; 192 | paddingX?: number; 193 | paddingY?: number; 194 | zIndex?: number; 195 | alerts?: Partial> & { 196 | input?: Partial; 197 | }; 198 | spinner?: Partial; 199 | } 200 | 201 | // Provider interface 202 | export interface I_PoptartProviderProps { 203 | children: ReactNode; 204 | config?: I_PoptartUserConfig; 205 | } 206 | 207 | export interface I_AlertButton { 208 | label: string; 209 | backgroundColor: string; 210 | onClick: () => void; 211 | } 212 | 213 | export enum E_AlertInputType { 214 | text = 'text', 215 | password = 'password', 216 | email = 'email', 217 | number = 'number', 218 | tel = 'tel', 219 | url = 'url', 220 | } 221 | 222 | export type T_AlertInputType = keyof typeof E_AlertInputType; 223 | 224 | export interface I_AlertInput { 225 | type?: T_AlertInputType; 226 | placeholder?: string; 227 | required?: boolean; 228 | validationCallback?: (value: string | undefined) => string | boolean; 229 | } 230 | 231 | export interface I_AlertProps { 232 | title: string; 233 | message: string; 234 | type?: T_PoptartType; 235 | confirmButtonLabel?: string; 236 | confirmButtonBackgroundColor?: string; 237 | confirmButtonCallback?: (value: string | undefined) => void; 238 | cancelButtonLabel?: string; 239 | cancelButtonBackgroundColor?: string; 240 | cancelButtonCallback?: () => void; 241 | showCancelButton?: boolean; 242 | showConfirmButton?: boolean; 243 | additionalButtons?: I_AlertButton[]; 244 | width?: string; 245 | backgroundColor?: string; 246 | fontColor?: string; 247 | animation?: T_PoptartAnimation; 248 | animationDuration?: number; 249 | input?: I_AlertInput; 250 | allowClickOffDismissal?: boolean; 251 | } 252 | -------------------------------------------------------------------------------- /src/Alert.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Icon from './Icon'; 3 | import AlertInput from './AlertInput'; 4 | 5 | import { getContrastColor } from './helpers'; 6 | 7 | import { I_PoptartConfig, I_AlertProps, I_AlertButton } from './types'; 8 | 9 | interface Props { 10 | config: I_PoptartConfig; 11 | alert: I_AlertProps; 12 | dismissAlert: () => void; 13 | } 14 | 15 | const FG_Z_INDEX = 2; 16 | const BG_Z_INDEX = 1; 17 | 18 | export default function Alert(props: Props) { 19 | const { config, alert, dismissAlert } = props; 20 | 21 | const [value, setValue] = useState(undefined); 22 | const [error, setError] = useState(undefined); 23 | 24 | const type = alert.type || config.alerts.defaultType; 25 | const primaryColor = config.colors[type]; 26 | const backgroundColor = alert.backgroundColor || config.alerts.defaultBackgroundColor; 27 | const fontColor = alert.fontColor || config.alerts.defaultFontColor; 28 | const animation = alert.animation || config.alerts.defaultAnimation; 29 | const animationDuration = alert.animationDuration || config.alerts.defaultAnimationDuration; 30 | const showCancelButton = 31 | alert.showCancelButton !== undefined ? alert.showCancelButton : config.alerts.defaultShowCancelButton; 32 | const showConfirmButton = 33 | alert.showConfirmButton !== undefined ? alert.showConfirmButton : config.alerts.defaultShowConfirmButton; 34 | 35 | const iconSize = config.alerts.iconSizeFactor * config.alerts.defaultFontSize; 36 | 37 | const confirmButtonBackgroundColor = alert.confirmButtonBackgroundColor || config.alerts.defaultConfirmButtonColor; 38 | const cancelButtonBackgroundColor = alert.cancelButtonBackgroundColor || config.alerts.defaultCancelButtonColor; 39 | 40 | const confirmButtonColor = getContrastColor({ 41 | backgroundColor: confirmButtonBackgroundColor, 42 | lightColor: config.colors.textLight, 43 | darkColor: config.colors.textDark, 44 | }); 45 | const cancelButtonColor = getContrastColor({ 46 | backgroundColor: cancelButtonBackgroundColor, 47 | lightColor: config.colors.textLight, 48 | darkColor: config.colors.textDark, 49 | }); 50 | 51 | const styles: { [key: string]: React.CSSProperties } = { 52 | alert: { 53 | pointerEvents: 'auto', 54 | position: 'relative', 55 | backgroundColor: backgroundColor, 56 | border: `${config.alerts.borderWidth}px solid ${primaryColor}`, 57 | color: fontColor, 58 | borderRadius: `${config.alerts.borderRadius}px`, 59 | boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)', 60 | width: alert.width || config.alerts.defaultWidth, 61 | minHeight: '300px', 62 | maxWidth: '90%', 63 | animation: `${animation} ${animationDuration}s ease-out`, 64 | }, 65 | title: { 66 | fontSize: `${config.alerts.defaultTitleFontSize}px`, 67 | color: primaryColor, 68 | }, 69 | buttonBase: { 70 | padding: '10px 20px', 71 | borderRadius: '5px', 72 | cursor: 'pointer', 73 | minWidth: '80px', 74 | }, 75 | titleContainer: { 76 | display: 'flex', 77 | alignItems: 'center', 78 | gap: '10px', 79 | zIndex: FG_Z_INDEX, 80 | }, 81 | alertMessage: { 82 | fontSize: `${config.alerts.defaultFontSize}px`, 83 | zIndex: FG_Z_INDEX, 84 | }, 85 | buttonGroup: { 86 | display: 'flex', 87 | justifyContent: 'center', 88 | gap: '16px', 89 | zIndex: FG_Z_INDEX, 90 | }, 91 | confirmButton: { 92 | backgroundColor: alert.confirmButtonBackgroundColor || config.alerts.defaultConfirmButtonColor, 93 | color: confirmButtonColor, 94 | }, 95 | cancelButton: { 96 | backgroundColor: alert.cancelButtonBackgroundColor || config.alerts.defaultCancelButtonColor, 97 | color: cancelButtonColor, 98 | }, 99 | background: { 100 | pointerEvents: 'none', 101 | position: 'absolute', 102 | inset: 0, 103 | opacity: 0.1, 104 | display: 'flex', 105 | alignItems: 'center', 106 | justifyContent: 'start', 107 | overflow: 'hidden', 108 | zIndex: BG_Z_INDEX, 109 | }, 110 | foreground: { 111 | display: 'flex', 112 | position: 'relative', 113 | flexDirection: 'column', 114 | alignItems: 'center', 115 | justifyContent: 'space-between', 116 | gap: '30px', 117 | width: '100%', 118 | minHeight: '300px', 119 | padding: `${config.alerts.paddingY}px ${config.alerts.paddingX}px`, 120 | }, 121 | backgroundImage: { 122 | transform: 'translateX(-100px)', 123 | }, 124 | }; 125 | 126 | const handleCancel = () => { 127 | if (alert.cancelButtonCallback) { 128 | alert.cancelButtonCallback(); 129 | } else { 130 | dismissAlert(); 131 | } 132 | }; 133 | 134 | const handleConfirm = () => { 135 | setError(undefined); 136 | 137 | if (alert.input && alert.input.required && !value) { 138 | setError('This field is required'); 139 | return; 140 | } 141 | 142 | if (alert.input && alert.input.validationCallback) { 143 | const validationError = alert.input.validationCallback(value); 144 | if (validationError !== true && typeof validationError === 'string') { 145 | setError(validationError); 146 | return; 147 | } 148 | } 149 | 150 | if (alert.confirmButtonCallback) { 151 | alert.confirmButtonCallback(value); 152 | } else { 153 | dismissAlert(); 154 | } 155 | }; 156 | 157 | const customButtons: I_AlertButton[] = []; 158 | if (alert.additionalButtons && typeof alert.additionalButtons === 'object') { 159 | customButtons.push(...alert.additionalButtons); 160 | } 161 | 162 | return ( 163 |
) => { 167 | e.stopPropagation(); 168 | }} 169 | > 170 |
171 |
172 |
173 | 174 |
175 |
176 |
177 | 178 |

{alert.title}

179 |
180 | {alert.message} 181 | {alert.input ? ( 182 | 192 | ) : null} 193 |
194 | {showConfirmButton ? ( 195 | 202 | ) : null} 203 | {showCancelButton || 204 | !!alert.cancelButtonLabel || 205 | !!alert.cancelButtonCallback || 206 | (alert?.input && alert.showCancelButton !== false) ? ( 207 | 214 | ) : null} 215 | {customButtons.map((button, index) => ( 216 | 232 | ))} 233 |
234 |
235 |
236 | ); 237 | } 238 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-poptart 2 | 3 | [![All Tests](https://github.com/designly1/react-poptart/actions/workflows/alltests.yml/badge.svg)](https://github.com/designly1/react-poptart/actions/workflows/alltests.yml) [![npm version](https://badge.fury.io/js/react-poptart.svg)](https://badge.fury.io/js/react-poptart) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/dwyl/esta/issues) 4 | 5 | [Changelog](https://github.com/designly1/react-poptart/blob/master/CHANGELOG.md) 6 | 7 | ## Overview 8 | 9 | `react-poptart` is an easy-to-use notification and alert system for React apps. It includes features such as multiple animations, auto-dismiss notifications, progress bars, customizable alerts, and flexible theming options. 10 | 11 | The bundled size of `react-poptart` is a mere 13kb, much smaller than most other libraries! 12 | 13 | This documentation will guide you through the setup, usage, and customization of the Poptart Notification and Alert components. 14 | 15 | ## Installation 16 | 17 | To install the package, use npm: 18 | 19 | ```bash 20 | npm install react-poptart 21 | ``` 22 | 23 | ## Setup 24 | 25 | To use the Poptart component in your React app, you must wrap your application in the `PoptartProvider`. This ensures that all child components can trigger notifications and alerts. 26 | 27 | ```tsx 28 | import React from 'react'; 29 | import { PoptartProvider } from 'react-poptart'; 30 | 31 | function App() { 32 | return ( 33 | 34 | 35 | 36 | ); 37 | } 38 | 39 | export default App; 40 | ``` 41 | 42 | ## Notifications Usage 43 | 44 | To trigger a notification, use the `usePoptart()` hook, which provides access to the `push()` and `dismiss()` methods. 45 | 46 | ### Example: 47 | 48 | ```tsx 49 | import React from 'react'; 50 | import { usePoptart } from 'react-poptart'; 51 | 52 | const NotificationButton = () => { 53 | const { push } = usePoptart(); 54 | 55 | const handleClick = () => { 56 | push({ 57 | type: 'success', 58 | message: 'This is a success notification!', 59 | duration: 5000, // Optional: Specify duration in milliseconds 60 | }); 61 | }; 62 | 63 | return ; 64 | }; 65 | ``` 66 | 67 | ### Promises 68 | 69 | You can also create a poptart with a loading spinner that resolves to either an error or success poptart: 70 | 71 | ```ts 72 | // Promise interface 73 | interface I_PoptartPromise { 74 | promise: Promise; 75 | successMessage: string; 76 | errorMessage: string; 77 | } 78 | 79 | // Promise method 80 | promise(message: string, promise: I_PoptartPromise, overrides?: Partial) => string; 81 | ``` 82 | 83 | ### Example: 84 | 85 | ```ts 86 | const handleCreatePromisePoptart = () => { 87 | poptart.promise('Saving user data...', { 88 | promise: new Promise(resolve => { 89 | setTimeout(() => { 90 | resolve(); 91 | }, 2000); 92 | }), 93 | successMessage: 'User data saved successfully!', 94 | errorMessage: 'Failed to save user data', 95 | }); 96 | }; 97 | ``` 98 | 99 | ## Poptart Properties 100 | 101 | | Property | Type | Default | Description | 102 | | -------------------- | ---------------- | ------------ | ----------------------------------------------------------------------------------------------------- | 103 | | `type?` | string | `'info'` | Type of the notification: `'info'`, `'success'`, `'error'`, `'warning'` | 104 | | `message` | string | N/A | The message displayed inside the notification | 105 | | `width?` | string | `'450px'` | Width of the notification | 106 | | `duration?` | number | `5000` | How long the notification stays visible (in milliseconds) | 107 | | `poptartStyle` | string | `'default'` | Style of poptart container. Options: `'default'`, `'filled'`, or `'inverted'`. Default is `'filled'`. | 108 | | `animation?` | string | `'bounceIn'` | Animation for the notification. Available options are `'fade'`, `'slide'`, `'bounceIn'`, etc. | 109 | | `animationDuration?` | number | `0.6` | Animation duration in seconds | 110 | | `onClick?` | function | `undefined` | Callback function for when a Poptart is clicked | 111 | | `promise?` | I_PoptartPromise | `undefined` | Create a promise poptart that resolves to either an error or success poptart (see promises section) | 112 | 113 | ## Alerts Usage 114 | 115 | You can also trigger alerts using the `alert()` method from `usePoptart()`. Alerts provide more detailed interactions like confirmation buttons and more. 116 | 117 | ### Example: 118 | 119 | ```tsx 120 | import React from 'react'; 121 | import { usePoptart } from 'react-poptart'; 122 | 123 | const AlertButton = () => { 124 | const { alert } = usePoptart(); 125 | 126 | const handleAlertClick = () => { 127 | alert({ 128 | title: 'Confirmation', 129 | message: 'Are you sure you want to proceed?', 130 | confirmButtonLabel: 'Yes', 131 | cancelButtonLabel: 'No', 132 | onConfirm: () => { 133 | console.log('Confirmed!'); 134 | }, 135 | onCancel: () => { 136 | console.log('Cancelled!'); 137 | }, 138 | }); 139 | }; 140 | 141 | return ; 142 | }; 143 | ``` 144 | 145 | ## Alert Properties 146 | 147 | | Property | Type | Default | Description | 148 | | ------------------------ | -------- | ------------- | ---------------------------------------------------------------------- | 149 | | `title` | string | N/A | The title of the alert | 150 | | `message` | string | N/A | The message displayed inside the alert | 151 | | `confirmButtonLabel?` | string | `'Ok'` | The label for the confirm button | 152 | | `cancelButtonLabel?` | string | `'Cancel'` | The label for the cancel button | 153 | | `onConfirm?` | function | N/A | Callback function when the confirm button is pressed | 154 | | `onCancel?` | function | N/A | Callback function when the cancel button is pressed | 155 | | `showCancelButton?` | boolean | `true` | Whether to show the cancel button | 156 | | `showConfirmButton?` | boolean | `true` | Whether to show the confirm button | 157 | | `defaultBackgroundColor?`| string | `#F5F5F5` | Background color of the alert | 158 | | `defaultFontColor?` | string | `#000` | Font color of the alert text | 159 | | `defaultAnimation?` | string | `'slideFromBottom'` | Animation for the alert display | 160 | | `defaultAnimationDuration?`| number | `0.25` | Animation duration in seconds | 161 | | `allowClickOffDismissal?`| boolean | `true` | Whether clicking outside the alert dismisses it | 162 | 163 | ## Alert Inputs 164 | 165 | In addition to basic alerts, `react-poptart` also supports input fields inside alerts, allowing for forms or other types of user input to be captured directly within the alert. 166 | 167 | ### Example: 168 | 169 | ```tsx 170 | import React from 'react'; 171 | import { usePoptart } from 'react-poptart'; 172 | 173 | const InputAlertButton = () => { 174 | const { alert } = usePoptart(); 175 | 176 | const handleAlertClick = () => { 177 | alert({ 178 | title: 'Enter Your Email', 179 | message: 'Please provide your email address.', 180 | input: { 181 | type: 'email', 182 | placeholder: 'your-email@example.com', 183 | required: true, 184 | }, 185 | confirmButtonLabel: 'Submit', 186 | cancelButtonLabel: 'Cancel', 187 | onConfirm: (value) => { 188 | console.log('Input value:', value); 189 | }, 190 | }); 191 | }; 192 | 193 | return ; 194 | }; 195 | ``` 196 | 197 | ### Alert Input Props 198 | 199 | | Property | Type | Default | Description | 200 | | ------------------------ | -------- | ----------- | ------------------------------------------- | 201 | | `type?` | string | `text` | The type of input (e.g., `text`, `email`) | 202 | | `placeholder?` | string | `""` | Placeholder text | 203 | | `required?` | boolean | `false` | Whether the input is required | 204 | | `validationCallback?` | function | `undefined` | Callback for custom input validation | 205 | 206 | ## Alert Inputs with Custom Validation 207 | 208 | You can add custom validation to alert inputs by passing a `validationCallback` to the input configuration. This function should return `true` if the input is valid or return a string message if the validation fails. The string message will be displayed as an error. 209 | 210 | ### Example with Custom Validation: 211 | 212 | ```tsx 213 | import React from 'react'; 214 | import { usePoptart } from 'react-poptart'; 215 | 216 | const CustomValidationAlertButton = () => { 217 | const { alert } = usePoptart(); 218 | 219 | const handleAlertClick = () => { 220 | alert({ 221 | title: 'Enter Your Email', 222 | message: 'Please provide your email address.', 223 | input: { 224 | type: 'email', 225 | placeholder: 'your-email@example.com', 226 | required: true, 227 | validationCallback: (value) => { 228 | const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 229 | if (!value) { 230 | return 'Email is required'; 231 | } else if (!emailPattern.test(value)) { 232 | return 'Please enter a valid email address'; 233 | } 234 | return true; 235 | }, 236 | }, 237 | confirmButtonLabel: 'Submit', 238 | cancelButtonLabel: 'Cancel', 239 | onConfirm: (value) => { 240 | console.log('Valid input:', value); 241 | }, 242 | }); 243 | }; 244 | 245 | return ; 246 | }; 247 | ``` 248 | 249 | ### Custom Validation Workflow: 250 | 251 | 1. The `validationCallback` function is passed to the alert input. 252 | 2. If the input is valid, the callback returns `true`. 253 | 3. If the input is invalid, the callback returns a string which will be displayed as an error message below the input field. 254 | 255 | ## Default Configuration 256 | 257 | Below is the default configuration for `react-poptart`. You can override these settings by passing a custom configuration object to the `PoptartProvider`. 258 | 259 | ```tsx 260 | export const defaultConfig: I_PoptartConfig = { 261 | // Default colors for the Poptart notifications 262 | colors: { 263 | success: '#229645', // Green color for success notifications 264 | error: '#e71b44', // Red color for error notifications 265 | warning: '#e9c514', // Yellow color for warning notifications 266 | info: '#1FA2FF', // Blue color for info notifications 267 | loading: '#4b5155', // Gray color for loading notifications 268 | textLight: '#f9f9f9', // Light contrasting color for text 269 | textDark: '#171717', // Dark contrasting color for text 270 | invertedBackground: '#f6f6f6', // Background color for inverted notifications 271 | }, 272 | // Override styles for various Poptart components 273 | styleOverrides: { 274 | container: {}, // Custom styles for the Poptart container 275 | poptart: {}, // Custom styles for individual Poptart notifications 276 | progressBar: {}, // Custom styles for the progress bar 277 | alertContainer: {}, // Custom styles for the alert container 278 | alert: {}, // Custom styles for the alert itself 279 | }, 280 | // Default alignment of Poptarts (possible values: 'tl' - top-left, 'tc' - top-center, 'tr' - top-right, 'bl' 281 | // - bottom-left, 'bc' - bottom-center, 'br' - bottom-right) 282 | defaultPoptartStyle: 'default', // Default style of the notification (possible values: 'default', 'filled', 'inverted') 283 | defaultAlign: 'br', 284 | // Default type of notification (possible values: 'success', 'error', 'warning', 'info') 285 | defaultType: 'info', 286 | // Default duration for which the notification is displayed (in milliseconds) 287 | defaultDuration: 5000, 288 | // Default width of the notification poptart 289 | defaultWidth: '450px', 290 | // Default animation for the notification appearance (possible values: 'bounceIn', 'fadeIn', 'slideFromLeft', 291 | // 'slideFromRight', 'slideFromTop', 'slideFromBottom') 292 | defaultAnimation: 'bounceIn', 293 | // Default animation duration for notifications (in seconds) 294 | defaultAnimationDuration: 0.6, 295 | // Default font size used in the notification text 296 | fontSize: 16, 297 | // Factor to adjust the size of icons relative to the font size 298 | iconSizeFactor: 2.5, 299 | // Customization for the progress bar in the notification 300 | progressBar: { 301 | lightColor: '#D6D6D6', // Color for the light theme of the progress bar 302 | darkColor: '#454545', // Color for the dark theme of the progress bar 303 | height: 5, // Height of the progress bar 304 | }, 305 | // Minimum contrast threshold for readability (used in calculating appropriate text color contrasts) 306 | contrastThreshold: 0.32, 307 | // Padding around the Poptart notification (X-axis) 308 | paddingX: 20, 309 | // Padding around the Poptart notification (Y-axis) 310 | paddingY: 16, 311 | // The z-index to control stacking order of the notification 312 | zIndex: 10, 313 | 314 | // Default settings for alerts (confirm/cancel dialog boxes) 315 | alerts: { 316 | defaultWidth: '600px', // Default width of the alert 317 | paddingX: 30, // Padding (X-axis) inside the alert box 318 | paddingY: 26, // Padding (Y-axis) inside the alert box 319 | borderRadius: 10, // Border radius for rounded corners of the alert box 320 | defaultType: 'info', // Default type of alert (info, success, error, or warning) 321 | defaultBackgroundColor: '#ffffff', // Default background color of the alert 322 | defaultFontColor: '#000', // Default text color inside the alert 323 | defaultFontSize: 20, // Default font size for the alert text 324 | defaultTitleFontSize: 28, // Font size for the alert title 325 | iconSizeFactor: 2, // Factor to adjust the size of icons within alerts 326 | borderWidth: 3, // Border width around the alert box 327 | defaultConfirmButtonColor: '#2d2d2d', // Color of the confirm button 328 | defaultCancelButtonColor: '#6B6B6B', // Color of the cancel button 329 | defaultConfirmButtonLabel: 'Ok', // Label text for the confirm button 330 | defaultCancelButtonLabel: 'Cancel', // Label text for the cancel button 331 | defaultShowCancelButton: false, // Whether the cancel button is shown by default 332 | defaultShowConfirmButton: true, // Whether the confirm button is shown by default 333 | defaultAnimation: 'slideFromBottom', // Default animation for alert boxes 334 | defaultAnimationDuration: 0.25, // Default duration for alert box animations (in seconds) 335 | allowClickOffDismissal: true, // Whether clicking outside the alert dismisses it 336 | 337 | // Default input styles for alerts (if input is used) 338 | input: { 339 | backgroundColor: '#fcfcfcac', // Background color for input fields in alerts 340 | fontColor: '#000', // Text color for input fields 341 | borderRadius: 5, // Border radius for the input field 342 | borderWidth: 1, // Border width around the input field 343 | paddingX: 10, // Padding (X-axis) for the input field 344 | paddingY: 8, // Padding (Y-axis) for the input field 345 | maxWidth: '70%', // Maximum width of the input field 346 | errorFeedbackColor: '#d12c2c', // Color used to indicate errors in input validation 347 | placeholderColor: '#a0a0a0', // Placeholder text color 348 | }, 349 | }, 350 | // Default settings for the spinner component 351 | spinner: { 352 | strokeWidth: 7, // Thickness of the spinner ring 353 | baseColor: '#f3f3f3', // Background color of the spinner ring 354 | accentColor: '#bbbbbb', // Accent color of the spinner ring 355 | animationDuration: 1, // Duration of the spinner animation (in seconds) 356 | }, 357 | }; 358 | ``` 359 | 360 | ## Dismissing Alerts 361 | 362 | Alerts can be dismissed by clicking on the cancel button, clicking off the alert if `allowClickOffDismissal` is set to `true`, or programmatically using `dismissAlert()`. 363 | 364 | ```tsx 365 | const { alert, dismissAlert } = usePoptart(); 366 | 367 | const handleDismiss = () => { 368 | dismissAlert(); 369 | }; 370 | ``` 371 | 372 | ## Custom Styling 373 | 374 | You can override the styles of both notifications and alerts by providing a `styleOverrides` object in the configuration. 375 | 376 | ```tsx 377 | 393 | 394 | 395 | ``` 396 | 397 | ## Development 398 | 399 | To develop `react-poptart`, simply clone and install dependencies and then run `pnpm run dev`. 400 | 401 | To test in a React development project, in the `react-poptart` root directory run: 402 | 403 | ``` 404 | pnpm link --global 405 | ``` 406 | 407 | Then in your React project root, run: 408 | 409 | ``` 410 | pnpm link react-poptart 411 | ``` 412 | 413 | When you're done with development, simply run: 414 | 415 | ``` 416 | pnpm unlink react-poptart 417 | ``` 418 | 419 | Run `pnpm run build` to build and `pnpm run test` to run all tests. 420 | 421 | ## Issues 422 | 423 | To report a bug or an issue, please use the [GitHub Repo Issues Tracker](https://github.com/designly1/react-poptart/issues). 424 | 425 | ## Contributing 426 | 427 | All pull requests are welcome. For major changes, please file an [issue](https://github.com/designly1/react-poptart/issues) first to discuss what you would like to change. 428 | --------------------------------------------------------------------------------