├── .npmrc ├── bun.lockb ├── vercel.json ├── .gitattributes ├── static ├── favicon.png ├── svoast-icon-mini.svg ├── svoast-icon-solid.svg ├── svoast-icon-outline.svg ├── svoast-icon-color.svg ├── svoast-logo-orange.svg ├── svoast-logo-for-dark.svg ├── svoast-logo-for-light.svg └── svoast-logo-color.svg ├── postcss.config.cjs ├── .vscode └── settings.json ├── .gitignore ├── src ├── site │ ├── stores.svelte.ts │ ├── utils.ts │ ├── CustomComponent.svelte │ └── TOC.svelte ├── lib │ ├── index.ts │ ├── utils.ts │ ├── Icon.svelte │ ├── Toasts.svelte │ ├── Toast.svelte │ ├── types.ts │ └── state.svelte.ts ├── routes │ ├── examples │ │ ├── _usage.svelte │ │ ├── _position.svelte │ │ ├── _promises.svelte │ │ ├── _custom.svelte │ │ ├── _types.svelte │ │ └── _options.svelte │ ├── changelog │ │ └── +page.svx │ ├── +layout.svelte │ └── +page.svx ├── app.css ├── app.d.ts └── app.html ├── .prettierignore ├── vite.config.ts ├── .prettierrc ├── tsconfig.json ├── tailwind.config.cjs ├── svelte.config.js ├── mdsvex.config.js ├── LICENSE.md ├── README.md └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gibbu/svoast/HEAD/bun.lockb -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gibbu/svoast/HEAD/static/favicon.png -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "[svelte]": { 4 | "editor.defaultFormatter": "svelte.svelte-vscode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | /dist -------------------------------------------------------------------------------- /src/site/stores.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { ToastPosition } from '$lib'; 2 | 3 | class SiteSettings { 4 | position = $state('bottom-right'); 5 | } 6 | 7 | export const settings = new SiteSettings(); 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | server: { 7 | fs: { 8 | allow: ['..'] 9 | } 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { ICON_PATHS, default as Icon } from './Icon.svelte'; 2 | export { default as Toast } from './Toast.svelte'; 3 | export { default as Toasts } from './Toasts.svelte'; 4 | export { toast } from './state.svelte'; 5 | 6 | export type * from './types'; 7 | -------------------------------------------------------------------------------- /src/routes/examples/_usage.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .btn, 6 | .select, 7 | .input { 8 | @apply px-4 h-12 rounded !bg-neutral-900 text-sm hover:!bg-neutral-800 hover:text-white transition-colors; 9 | } 10 | 11 | .shiki { 12 | @apply !bg-transparent; 13 | } 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 120, 6 | "tabWidth": 2, 7 | "plugins": ["prettier-plugin-svelte"], 8 | "pluginSearchDirs": ["."], 9 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 10 | } 11 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{html,js,svelte,ts}'], 4 | theme: { 5 | extend: { 6 | fontFamily: { 7 | body: ['Inter', 'Roboto', 'Arial', 'sans-serif'], 8 | display: ['Manrope', 'Inter', 'Roboto', 'Arial', 'sans-serif'] 9 | } 10 | } 11 | }, 12 | plugins: [require('@tailwindcss/typography')] 13 | }; 14 | -------------------------------------------------------------------------------- /src/routes/examples/_position.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { mdsvex } from 'mdsvex'; 2 | import mdsvexConfig from './mdsvex.config.js'; 3 | import adapter from '@sveltejs/adapter-vercel'; 4 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 5 | 6 | /** @type {import('@sveltejs/kit').Config} */ 7 | const config = { 8 | extensions: ['.svelte', ...mdsvexConfig.extensions], 9 | preprocess: [vitePreprocess(), mdsvex(mdsvexConfig)], 10 | kit: { 11 | adapter: adapter(), 12 | alias: { 13 | $site: './src/site/*' 14 | } 15 | } 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /src/site/utils.ts: -------------------------------------------------------------------------------- 1 | export const debounce = (fn: Function, ms = 300) => { 2 | let timeoutId: ReturnType; 3 | return function (this: any, ...args: any[]) { 4 | clearTimeout(timeoutId); 5 | timeoutId = setTimeout(() => fn.apply(this, args), ms); 6 | }; 7 | }; 8 | 9 | export const fakeApi = (min: number, max: number) => { 10 | return new Promise((resolve, reject) => { 11 | const success = Math.random() < 0.5; 12 | 13 | setTimeout(() => { 14 | if (success) resolve(true); 15 | else reject(false); 16 | }, Math.floor(Math.random() * (max - min + 1) + min) * 1000); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/routes/examples/_promises.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /mdsvex.config.js: -------------------------------------------------------------------------------- 1 | import { defineMDSveXConfig as defineConfig, escapeSvelte } from 'mdsvex'; 2 | import slug from 'rehype-slug'; 3 | import externalLinks from 'remark-external-links'; 4 | import { codeToHtml } from 'shiki'; 5 | 6 | const config = defineConfig({ 7 | extensions: ['.svx'], 8 | 9 | highlight: { 10 | highlighter: async (code, lang = 'text') => { 11 | const returned = await codeToHtml(code, { 12 | theme: 'github-dark', 13 | lang 14 | }); 15 | const html = escapeSvelte(returned); 16 | return `{@html \`${html}\` }`; 17 | } 18 | }, 19 | 20 | smartypants: { 21 | dashes: 'oldschool' 22 | }, 23 | 24 | remarkPlugins: [[externalLinks, { target: '_blank', rel: 'noopener noreferrer' }]], 25 | rehypePlugins: [slug] 26 | }); 27 | 28 | export default config; 29 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | SVoast 14 | 15 | 16 | 17 | %sveltekit.head% 18 | 19 | 20 |
%sveltekit.body%
21 | 22 | 23 | -------------------------------------------------------------------------------- /src/routes/examples/_custom.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/routes/examples/_types.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 |
26 | 31 | 32 | 33 |
34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Gibbu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | A simple toast component for Svelte. 7 |

8 | 9 | --- 10 | 11 | ## Usage 12 | 13 | ```svelte 14 | 15 | 18 | 19 | 20 | ``` 21 | 22 | ```svelte 23 | 24 | 31 | 32 | 33 | ``` 34 | 35 | ## Docs 36 | 37 | View more information at: https://svoast.vercel.app 38 | 39 |
40 | 41 | ## Changelog 42 | 43 | You can view all the changes at https://svoast.vercel.app/changelog 44 | 45 |
46 | 47 | ## Licence 48 | 49 | See the [LICENSE](https://github.com/Gibbu/svoast/blob/main/LICENSE) file for license rights and limitations (MIT). 50 | 51 |
52 | 53 | ## Credits 54 | 55 | SVoast logo made by [Bruce Wayyn](https://github.com/brucewayyn). 56 | -------------------------------------------------------------------------------- /src/site/CustomComponent.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 |
39 |
40 |

{message}

41 | {#if type !== 'promise'} 42 | 48 | Open 49 | 50 | {/if} 51 |
52 | {#if type !== 'promise'} 53 |
54 | {/if} 55 |
56 | 57 | 70 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ToastComponentOptions, ToastAnimation, ToastFunctionOptions } from './types'; 2 | 3 | let id: number = 1; 4 | 5 | /** 6 | * Generate a unique ID for the toasts. 7 | */ 8 | export const ID = (): number => id++; 9 | 10 | /** 11 | * The default options of SVoast. 12 | */ 13 | export const DEFAULT_OPTIONS: ToastComponentOptions = { 14 | closable: false, 15 | duration: 5000, 16 | infinite: false, 17 | rich: false 18 | }; 19 | 20 | /** 21 | * Default animation for the toasts. 22 | */ 23 | export const DEFAULT_ANIMATION: ToastAnimation = { 24 | start: 0.75, 25 | opacity: 0, 26 | duration: 150 27 | }; 28 | 29 | /** 30 | * Transforms the string value to a number. 31 | * 32 | * Supported identifiers: `ms`, `s` 33 | * @param value The value to be transformed. 34 | */ 35 | export const parseDuration = (value: number | string): number => { 36 | if (typeof value === 'number') return value; 37 | if (!/ms|s$/.test(value)) 38 | throw new Error('[SVoast] `duration` prop was given a string but not a leading identifier (ms/s).'); 39 | 40 | const duration: number = parseFloat(value.split(/ms|s/)[0]); 41 | 42 | if (/(?=ms)(?!s)/.test(value)) return duration; 43 | return duration * 1000; 44 | }; 45 | 46 | /** 47 | * Shallow merges two objects while replacing the same keys. 48 | * @param original The original object to be overwritten. 49 | * @param newObject The object to be added to the original object, replacing existing keys. 50 | */ 51 | export const objectMerge = , TNew extends Record | undefined>( 52 | original: TOriginal, 53 | newObject: TNew 54 | ): TOriginal & TNew => { 55 | return { 56 | ...original, 57 | ...newObject 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /src/routes/examples/_options.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 |
30 | 31 | 32 |
33 | 37 | 41 | 45 |
46 |
47 |
48 | 49 | 50 | 51 |
52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svoast", 3 | "version": "3.0.3", 4 | "scripts": { 5 | "dev": "vite dev", 6 | "build:site": "vite build", 7 | "package": "svelte-kit sync && svelte-package -o package", 8 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 9 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 10 | "lint": "prettier --plugin-search-dir . --check .", 11 | "format": "prettier --plugin-search-dir . --write .", 12 | "push": "npm run package && npm publish" 13 | }, 14 | "keywords": [ 15 | "toast", 16 | "svelte", 17 | "svelte-component" 18 | ], 19 | "exports": { 20 | ".": { 21 | "types": "./package/index.d.ts", 22 | "svelte": "./package/index.js" 23 | } 24 | }, 25 | "files": [ 26 | "package", 27 | "!package/**/*.test.*", 28 | "!package/**/*.spec.*" 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/Gibbu/svoast.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/Gibbu/svoast/issues" 36 | }, 37 | "homepage": "https://svoast.vercel.app", 38 | "devDependencies": { 39 | "@sveltejs/adapter-vercel": "^5.4.7", 40 | "@sveltejs/kit": "^2.8.1", 41 | "@sveltejs/package": "^2.3.7", 42 | "@sveltejs/vite-plugin-svelte": "^4.0.1", 43 | "@tailwindcss/typography": "^0.5.15", 44 | "autoprefixer": "^10.4.20", 45 | "clsx": "^2.1.1", 46 | "fenceparser": "^2.2.0", 47 | "mdsvex": "^0.12.3", 48 | "postcss": "^8.4.49", 49 | "prettier": "^3.3.3", 50 | "prettier-plugin-svelte": "^3.2.8", 51 | "rehype-slug": "^6.0.0", 52 | "remark-code-extra": "^1.0.1", 53 | "remark-external-links": "9.0.1", 54 | "shiki": "^1.23.1", 55 | "shiki-twoslash": "^3.1.2", 56 | "svelte": "^5.2.3", 57 | "svelte-check": "^4.0.9", 58 | "tailwindcss": "^3.4.15", 59 | "tslib": "^2.8.1", 60 | "typescript": "^5.6.3", 61 | "vite": "^5.4.11" 62 | }, 63 | "peerDependencies": { 64 | "svelte": "^5.0.0" 65 | }, 66 | "type": "module" 67 | } 68 | -------------------------------------------------------------------------------- /src/lib/Icon.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/site/TOC.svelte: -------------------------------------------------------------------------------- 1 | 55 | 56 | 57 | 58 | 76 | -------------------------------------------------------------------------------- /src/lib/Toasts.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 | {#each toaster.toasts as toast (toast.id)} 24 |
31 | {#if toast?.component?.[0] || component} 32 | {@const { component: _, ...props } = toast} 33 | {@const Rendered = toast?.component?.[0] || component} 34 | 35 | {:else} 36 | 37 | {/if} 38 |
39 | {/each} 40 |
41 | 42 | 96 | -------------------------------------------------------------------------------- /src/routes/changelog/+page.svx: -------------------------------------------------------------------------------- 1 | [Back to docs](/) 2 | 3 | # SVoast changelog 4 | 5 | ## 3.0.0 6 | 7 | - Svelte 5 support. 8 | 9 | This is a complete convert over to Svelte 5. 10 | Svelte 4 **will not** work with this update. 11 | 12 | ## 2.5.0 13 | 14 | - Returned original promise with `toast.promise` (thanks [andogq](https://github.com/andogq)!) 15 | - Allowed `toast.promise` `success` and `error` config options to be a callback with the resulting data passed though. (thanks [andogq](https://github.com/andogq)!) 16 | 17 | ## 2.4.5 18 | 19 | - Added timeout tracking to reduce any unnecessary timeout calls. 20 | 21 | ## 2.3.0 22 | 23 | - Added `onStart`, `onSuccess`, `onError`, and `onFinish` life cycle hooks to the promise toast. 24 | 25 | ## 2.2.0 26 | 27 | - New `promise` method. 28 | 29 | ## 2.1.3 30 | 31 | - Fixed global components. 32 | 33 | ## 2.1.2 34 | 35 | - Better handling of default options and animations. 36 | - Better comments on types and props. 37 | 38 | ## 2.1.1 39 | 40 | - Fixed default icons being shrunk by max width. 41 | 42 | ## 2.1.0 43 | 44 | - Allowed for string duration timers. To allow for better clarity for those who wish. 45 | - `1.25s` = `1250` 46 | - `150ms` = `150` 47 | - `0.75s` = `750` 48 | 49 | ## 2.0.0 50 | 51 | - Merged `Attention/Check/Cross/Info/Warning.svelte` into a single `Icon.svelte` which receives a `ToastType` and looks up the SVG icon data from a map. 52 | 53 | ## 1.3.0 54 | 55 | - Added `rich` option to allow for rich HTML content in the message. 56 | - Added `onMount` and `onRemove` life cycle hooks. 57 | - `removeById` checks if ID exists in store before removing. 58 | 59 | ## 1.2.1 60 | 61 | - Added `removeAll` method. 62 | - Made `removeById` and `removeByIndex` synchronous. 63 | 64 | ## 1.2.0 65 | 66 | - Added `infinite` prop. 67 | - Renamed `remove` to `removeById` to provide better clairity. 68 | - Added `removeByIndex` which will, remove the toast by the given index. 69 | 70 | ## 1.1.0 71 | 72 | - Rewrite of internal code. 73 | - This doesn't change anything user side but makes it much easier to maintain. 74 | - `opts` passed prop has been replaced by extracting both options to their own separate prop. 75 | - `export let opts`, `opts.duration` > `export let duration`, `duration`. 76 | - The `component` option is no longer passed to the toast component. 77 | 78 | ## 1.0.0 79 | 80 | - Allowed the use of custom components. 81 | - Read the [Docs](https://svoast.vercel.app/#custom-components) to learn more. 82 | 83 | ## 0.1.1 84 | 85 | - Exported all used icons. 86 | 87 | ## 0.1.0 88 | 89 | - Added icon to match the type of toast. 90 | - Fixed smaller toasts stretching to the the width of larger toasts. 91 | 92 | ## 0.0.1 93 | 94 | - Hmmmm toast.... 95 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | {#key settings.position} 24 | 25 | {/key} 26 | 27 |
28 |
32 |
33 |

34 | SVoast 35 | v{version} 36 |

37 |

A simple toast component for Svelte

38 |
39 | 46 | 47 | 50 | 51 | 52 |
53 | 54 |
57 |
69 | 70 |
71 | {#key $page} 72 | 73 | {/key} 74 |
75 | -------------------------------------------------------------------------------- /src/lib/Toast.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
17 |
18 |
19 | 20 |
21 |
22 | {#if toast.rich} 23 | {@html toast.message} 24 | {:else} 25 | {toast.message} 26 | {/if} 27 |
28 | {#if toast.closable && toast.type !== 'promise'} 29 | 34 | {/if} 35 |
36 | 37 | 104 | -------------------------------------------------------------------------------- /static/svoast-icon-mini.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /static/svoast-icon-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from 'svelte'; 2 | 3 | /** 4 | * The position of the toasts. 5 | * 6 | * The will also effect how the toasts stack on each other. 7 | */ 8 | export type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'; 9 | 10 | /** The type of toast. */ 11 | export type ToastType = 'info' | 'attention' | 'success' | 'warning' | 'error' | 'promise'; 12 | 13 | /** Simple helper to define the internal functions. */ 14 | export type ToastFunction = (message: string, opts?: ToastFunctionOptions) => void; 15 | 16 | export type ToastPromiseFunction = (promise: Promise, opts: ToastPromiseOptions) => Promise; 17 | 18 | /** 19 | * The custom component to rendered. 20 | * 21 | * First index will be the component.\ 22 | * Second index will be any props you wish to pass down to your component. 23 | */ 24 | export type ToastCustomComponent> = [Component, Record]; 25 | 26 | /** Toast component props. */ 27 | export type ToastComponentOptions = Required>; 28 | 29 | export interface ToastAddOptions { 30 | id?: number; 31 | opts?: ToastFunctionOptions; 32 | } 33 | 34 | export interface ToastPromiseOptions extends Partial { 35 | /** The loading message of the promise. */ 36 | loading: string; 37 | /** The text to be displayed if the promise is resolved. */ 38 | success: string | ((data: T) => string); 39 | /** The text to be displayed if the promise is rejected. */ 40 | error: string | ((error: unknown) => string); 41 | /** Function the run when the promise has started. */ 42 | onStart?: () => void; 43 | /** 44 | * Function to run when the promise has been resolved. 45 | * @param data Any data returned back from the request. 46 | */ 47 | onSuccess?: (data: T) => void; 48 | /** 49 | * Function to run when the promise has been rejected. 50 | * @param data Any data returned back from the request. 51 | */ 52 | onError?: (data: T) => void; 53 | /** Function to run when the promise has ended, no matter the result. */ 54 | onFinish?: () => void; 55 | } 56 | 57 | /** The toast animation properties. */ 58 | export interface ToastAnimation { 59 | /** The starting scale size. */ 60 | start: number; 61 | /** The starting opacity. */ 62 | opacity: number; 63 | /** How long it should take for the toast to be done with the animation. */ 64 | duration: number; 65 | } 66 | 67 | export interface ToastLifeCycles { 68 | /** Run when the toast has been added to the store. */ 69 | onMount?: () => void; 70 | /** Run when the toast has been removed from the store. */ 71 | onRemove?: () => void; 72 | } 73 | 74 | export interface ToastFunctionOptions = any> extends ToastLifeCycles { 75 | /** Allow the toast to be dismissed. */ 76 | closable?: boolean; 77 | /** 78 | * The duration of the toast in milliseconds. 79 | * 80 | * Can also accept string values such as: `1s`, `1.75s`, `1500ms`. 81 | * 82 | * The duration used for a promise type will be the duration of the success or error toast. 83 | */ 84 | duration?: number | string; 85 | /** 86 | * The custom component to rendered. 87 | * 88 | * First index will be the component.\ 89 | * Second index will be any props you wish to pass down to your component. 90 | */ 91 | component?: ToastCustomComponent; 92 | /** Never remove the toast. */ 93 | infinite?: boolean; 94 | /** 95 | * Allow the use of HTML in the message.\ 96 | * **CAUTION**: Make sure to sanitize ALL content provided by users. 97 | */ 98 | rich?: boolean; 99 | } 100 | 101 | export interface ToastComponent extends ToastComponentOptions { 102 | /** The unique ID of the toast. */ 103 | id: number; 104 | /** The attention level of the toast. */ 105 | type: ToastType; 106 | /** The message to display to the end user. */ 107 | message: string; 108 | } 109 | 110 | /** Object interface to tell if there's a `component` prop. */ 111 | export interface ToastComponentWithCustom = any> extends ToastComponent { 112 | component: ToastCustomComponent; 113 | } 114 | -------------------------------------------------------------------------------- /src/lib/state.svelte.ts: -------------------------------------------------------------------------------- 1 | import { ID, DEFAULT_OPTIONS, parseDuration, objectMerge } from './utils'; 2 | 3 | import type { 4 | ToastFunction, 5 | ToastType, 6 | ToastPosition, 7 | ToastFunctionOptions, 8 | ToastComponentWithCustom, 9 | ToastPromiseFunction, 10 | ToastAddOptions 11 | } from './types'; 12 | 13 | class ToastState { 14 | toasts = $state([]); 15 | position = $state('bottom-left'); 16 | #timeouts = new Map(); 17 | 18 | #addToast(type: ToastType, message: string, { opts, id }: ToastAddOptions) { 19 | const uuid = id || ID(); 20 | 21 | const customProps: Record = opts?.component?.[1] || {}; 22 | const { closable, component, infinite, rich, onMount, onRemove, duration } = objectMerge( 23 | DEFAULT_OPTIONS, 24 | opts 25 | ) as Required; 26 | const DURATION = parseDuration(duration); 27 | 28 | const props: ToastComponentWithCustom = { 29 | id: uuid, 30 | type, 31 | message, 32 | duration: DURATION, 33 | closable, 34 | component, 35 | infinite, 36 | rich, 37 | ...customProps 38 | }; 39 | 40 | if (typeof window !== 'undefined') onMount?.(); 41 | 42 | this.#upsert(props, uuid); 43 | 44 | if (!infinite && type !== 'promise') { 45 | this.#timeouts.set( 46 | uuid, 47 | setTimeout(() => { 48 | this.removeById(uuid); 49 | onRemove?.(); 50 | }, DURATION) 51 | ); 52 | } 53 | 54 | return uuid; 55 | } 56 | #upsert(props: ToastComponentWithCustom, id: number) { 57 | if (this.toasts.find((toast) => toast.id === id)) { 58 | this.toasts = this.toasts.map((toast) => { 59 | if (toast.id === id) return { ...toast, ...props }; 60 | return toast; 61 | }); 62 | } else { 63 | this.toasts = this.position.includes('bottom') ? [...this.toasts, props] : [props, ...this.toasts]; 64 | } 65 | } 66 | #removeTimeout(timeoutId: number) { 67 | const timeout = this.#timeouts.get(timeoutId); 68 | if (timeout) { 69 | clearTimeout(timeout); 70 | this.#timeouts.delete(timeoutId); 71 | } 72 | } 73 | 74 | removeById(id: number) { 75 | const toast = this.toasts.find((toast) => toast.id === id); 76 | if (!toast) return; 77 | 78 | this.toasts = this.toasts.filter((toast) => toast.id !== id); 79 | 80 | this.#removeTimeout(id); 81 | } 82 | removeByIndex(index: number) { 83 | const toast = this.toasts[index]; 84 | if (!toast) return; 85 | 86 | this.toasts = this.toasts.filter((_, i) => index !== i); 87 | 88 | this.#removeTimeout(toast.id); 89 | } 90 | removeAll() { 91 | this.toasts = []; 92 | this.#timeouts.clear(); 93 | } 94 | 95 | info: ToastFunction = (message, opts = DEFAULT_OPTIONS) => this.#addToast('info', message, { opts }); 96 | attention: ToastFunction = (message, opts = DEFAULT_OPTIONS) => this.#addToast('attention', message, { opts }); 97 | success: ToastFunction = (message, opts = DEFAULT_OPTIONS) => this.#addToast('success', message, { opts }); 98 | warning: ToastFunction = (message, opts = DEFAULT_OPTIONS) => this.#addToast('warning', message, { opts }); 99 | error: ToastFunction = (message, opts = DEFAULT_OPTIONS) => this.#addToast('error', message, { opts }); 100 | promise: ToastPromiseFunction = (promise, opts) => { 101 | if (promise instanceof Promise === false) throw Error('`promise` is not a valid Promise.'); 102 | 103 | const id = this.#addToast('promise', opts.loading, { opts }); 104 | 105 | opts.onStart?.(); 106 | 107 | promise 108 | .then((data) => { 109 | const message = typeof opts.success === 'string' ? opts.success : opts.success(data); 110 | this.#addToast('success', message, { opts, id }); 111 | opts?.onSuccess?.(data); 112 | }) 113 | .catch((err) => { 114 | const message = typeof opts.error === 'string' ? opts.error : opts.error(err); 115 | this.#addToast('error', message, { opts, id }); 116 | opts?.onError?.(err); 117 | }) 118 | .finally(() => { 119 | if (!opts?.infinite) { 120 | setTimeout( 121 | () => { 122 | this.removeById(id); 123 | }, 124 | parseDuration(opts.duration || DEFAULT_OPTIONS.duration) 125 | ); 126 | } 127 | opts?.onFinish?.(); 128 | }); 129 | 130 | return promise; 131 | }; 132 | } 133 | 134 | export const toast = new ToastState(); 135 | -------------------------------------------------------------------------------- /static/svoast-icon-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /static/svoast-icon-color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /static/svoast-logo-orange.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /static/svoast-logo-for-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /static/svoast-logo-for-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /static/svoast-logo-color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/routes/+page.svx: -------------------------------------------------------------------------------- 1 | 9 | 10 | # Features 11 | 12 | - SvelteKit and SSR compatible. 13 | - Zero Dependencies. 14 | - Built using TypeScript. All types are exported. 15 | - Themeing support using CSS variables. 16 | - Optional custom components. 17 | - Effortless setup. Import the component, use the function. 18 | 19 | --- 20 | 21 | # Installation 22 | 23 | Install using your favourite package manager: 24 | 25 | ``` 26 | npm i svoast -D 27 | ``` 28 | 29 | ``` 30 | yarn add svoast -D 31 | ``` 32 | 33 | ``` 34 | pnpm add svoast -D 35 | ``` 36 | 37 | --- 38 | 39 | # Usage 40 | 41 | Import the `` component to your main layout file. 42 | 43 | ```svelte filename="routes/+layout.svelte" 44 | 47 | 48 | 49 | ``` 50 | 51 | And then call the `toast` function anywhere! (on the client, of course) 52 | 53 | ```svelte filename="routes/+page.svelte" 54 | 61 | 62 | 63 | ``` 64 | 65 | 66 | 67 | --- 68 | 69 | # Attention levels 70 | 71 | You have 5 levels of toasts to play with. 72 | 73 | Info, attention, success, warning, and error. 74 | These _should_ be enough to express to the user what they mean. 75 | If not, check out the [custom components](#custom-components) docs where you can completely customize the look and functionality of your toasts. 76 | 77 | 78 | 79 | --- 80 | 81 | # Options 82 | 83 | There are 4 options that you can pass: 84 | 85 | - `closable` will allow the user to dismiss the toast early. 86 | - `duration` well, will be the time the toast is visible. 87 | - `infinite` will ignore `duration` and will never be removed unless you give the user a dismiss button. 88 | - `rich` will render the message as a HTML string. So providing `Warning: there was an error.` will render the `strong` tags. 89 | 90 | 91 | 92 | --- 93 | 94 | # Positioning 95 | 96 | The `` component has a `position` prop that allows you to change what position the toasts are displayed from. 97 | This will change what direction they popout from and how they stack on each other. 98 | 99 | 100 | 101 | --- 102 | 103 | # Styling 104 | 105 | SVoast uses CSS variables. 106 | 107 | ```css 108 | :root { 109 | /* Spacing for the container and between each toast */ 110 | --svoast-offset: 16px; 111 | --svoast-gap: 16px; 112 | 113 | /* The toast itself. */ 114 | --svoast-bg: #333; 115 | --svoast-text: #fff; 116 | --svoast-padding: 10px 15px 10px 18px; 117 | --svoast-radius: 4px; 118 | --svoast-shadow: 0 2px 7px hsl(0 0% 0% / 0.25); 119 | --svoast-font-size: 14px; 120 | --svoast-dismiss-gap: 8px; 121 | --svoast-max-width: unset; 122 | 123 | /* The current colour of the toast, depending on the type */ 124 | --svoast-colour: ''; 125 | 126 | /* Colour for each type */ 127 | --svoast-info-colour: #888; 128 | --svoast-attention-colour: #38bdf8; 129 | --svoast-success-colour: #4ade80; 130 | --svoast-warning-colour: #fb923c; 131 | --svoast-error-colour: #ef4444; 132 | 133 | /* The coloured bar */ 134 | --svoast-bar-width: 3px; 135 | 136 | /* Icons */ 137 | --svoast-icon-padding: 2px; 138 | } 139 | ``` 140 | 141 | --- 142 | 143 | # Promises 144 | 145 | If you're doing any api calls and want to alert to the user something is happening in the background, you can provide the `promise` type. 146 | 147 | There are 3 required additional options: 148 | 149 | - `loading` is the text while the promise is pending. 150 | - `success` will be the text if the promise is resolved. 151 | - `error` is the text if the promise is rejected. 152 | 153 | ```svelte filename="routes/settings/+page.svelte" 154 | 166 | 167 | 168 | ``` 169 | 170 | 171 | 172 | --- 173 | 174 | # Custom Components 175 | 176 | If you want a completely new look and/or want to add functionality to the toasts, you can either do it globally or individually. 177 | 178 | ### Global toasts 179 | 180 | Providing a component to the `component` prop on the `` component will overwrite the default toasts. 181 | There's a draw back to this though; you will be stuck with the props provided by this package. 182 | View the [API](#api) table below to see what the default props are. 183 | 184 | ```svelte filename="routes/+layout.svelte" 185 | 189 | 190 | 191 | ``` 192 | 193 | --- 194 | 195 | ### Individual toasts 196 | 197 | If you require to display a specific toast for an action, provide the `component` option on the `toast` call. 198 | The first index of the array will be the custom component you want to use. 199 | The second index of the array will be an object of any props you want to be passed down to your component. 200 | 201 | **NOTE**: All props provided by this package are still passed down to your custom component (so no need to redefine everything). 202 | 203 | ```svelte filename="lib/LinkToast.svelte" 204 | 217 | 218 |
219 |

{message}

220 | View Page 221 |
222 | ``` 223 | 224 | ```svelte {6,2} filename="routes/settings/+page.svelte" 225 | 235 | 236 | 237 | ``` 238 | 239 | 240 | 241 | --- 242 | 243 | ### Custom dismiss button 244 | 245 | If you want a dismiss button, make sure to export an `id` prop in your custom component. 246 | And then you can use the `removeById` method on the `toast` function. 247 | 248 | ```svelte {3,9} filename="lib/Custom.svelte" 249 | 255 | 256 |
257 | {message} 258 | 259 |
260 | ``` 261 | 262 | --- 263 | 264 | # Life cycle hooks 265 | 266 | There are a number of hooks that you can use to run code when a certain action has happened. 267 | For normal toasts, you have the `onMount` and `onRemove` methods. 268 | 269 | For the promise toasts, you have the two mentioned before and `onStart`, `onSuccess`, `onError`, `onFinish`. 270 | These should be pretty self-explanatory on when they run. 271 | 272 | Simply pass these into the options object of your toast call. 273 | 274 | --- 275 | 276 | # API 277 | 278 | ### Methods 279 | 280 | | Name | Description | 281 | | ------------- | -------------------------------------------------------------------- | 282 | | info | Usually indicates information to the user, but isn't important. | 283 | | attention | Indicate to the user with important information. | 284 | | succcess | Indicates to the user something good has happened. | 285 | | warning | Tell the user something may be wrong but isn't critical. | 286 | | error | Alert the user something critical has happened. | 287 | | promise | Indicates to the user that something is happening in the background. | 288 | | removeById | Remove the toast by the unique ID. | 289 | | removeByIndex | Remove the toast by index position. | 290 | | removeAll | Remove all toasts. | 291 | 292 | ### Options 293 | 294 | | Property | Type | Description | 295 | | --------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------- | 296 | | closable | `boolean` | Allow the toast to be dismissed. | 297 | | duration | `number | string` | The duration of the toast in milliseconds. After said time, the toast will remove itself. | 298 | | component | `ToastCustomComponent` | Allow a custom component to be rendered. | 299 | | infinite | `boolean` | Ignores `duration` and will never expire unless dismissed or removed by any of the `remove` methods. | 300 | | rich | `boolean` | Allow the use of HTML in the message.
**CAUTION**: Make sure to sanitize ALL content provided by users. | 301 | 302 | ### Exported props 303 | 304 | These are the props that are available to use if you wish to use a custom component. 305 | 306 | | Name | Type | 307 | | -------- | ---------------------- | 308 | | id | `number` | 309 | | type | `ToastType` | 310 | | message | `string` | 311 | | closable | `boolean` | 312 | | duration | `number | string` | 313 | | infinite | `boolean` | 314 | 315 | ### Life Cycle Hooks 316 | 317 | | Name | Parameters | Type | Description | 318 | | ----------- | ---------- | ---------------------------- | ------------------------------------------------- | 319 | | `onMount` | | `() => void` | When a toast has been added to the store. | 320 | | `onRemove` | | `() => void` | When a toast has been removed from the store. | 321 | | `onStart` | | `() => void` | When the promise has been started. | 322 | | `onSuccess` | `data` | `(data: T) => void` | When the promise has been resolved. | 323 | | `onError` | `data` | `(data: T) => void` | When the promise has been rejected. | 324 | | `onFinish` | | `() => void` | When the promise has ended, no matter the result. | 325 | 326 | --- 327 | 328 | # Changelog 329 | 330 | You can view all the changes [HERE](/changelog) 331 | --------------------------------------------------------------------------------