| <!-- |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| --> |
| |
| <script lang="ts" module> |
| import { flip } from 'svelte/animate'; |
| import { writable } from 'svelte/store'; |
| import { fly, fade } from 'svelte/transition'; |
| import { cubicOut } from 'svelte/easing'; |
| import { twMerge } from 'tailwind-merge'; |
| import type { iconType } from './Icon.svelte'; |
| import Icon from './Icon.svelte'; |
| |
| type ToastType = 'success' | 'error' | 'info'; |
| |
| type Toast = { |
| id: number; |
| duration: number; |
| type: ToastType; |
| description: string; |
| remove: () => void; |
| }; |
| |
| const toastsStore = writable<Toast[]>([]); |
| |
| export function showToast({ |
| description, |
| duration = 2200, |
| type |
| }: { |
| duration?: number; |
| type: ToastType; |
| |
| description: string; |
| }): Promise<void> { |
| const toastId = Date.now(); |
| const obj = { |
| id: toastId, |
| duration, |
| description, |
| type, |
| remove: () => toastsStore.update((v) => v.filter((i) => i.id !== toastId)) |
| }; |
| |
| toastsStore.update((v) => [...v, obj]); |
| |
| // 0 seconds duration means toast will never disappear |
| return new Promise((res) => { |
| if (duration > 0) { |
| setTimeout(() => { |
| obj.remove(); |
| res(); |
| }, duration); |
| } |
| }); |
| } |
| |
| const icons = { |
| success: 'checkCircle', |
| error: 'alertCircle', |
| info: 'infoCircle' |
| } satisfies Record<ToastType, iconType>; |
| |
| const title = { |
| success: 'Success', |
| error: 'Something went wrong' |
| }; |
| </script> |
| |
| <div class="fixed p-2 pointer-events-none bottom-5 right-5 z-50"> |
| <ul class="flex flex-col gap-2"> |
| {#each $toastsStore as { id, remove, type, description } (id)} |
| <li |
| animate:flip={{ duration: 250, easing: cubicOut }} |
| in:fly={{ duration: 300, x: '70%', easing: cubicOut }} |
| out:fade={{ duration: 300, easing: cubicOut }} |
| > |
| <div |
| role="alert" |
| aria-live="assertive" |
| aria-atomic="true" |
| class={twMerge( |
| 'p-3 flex gap-4 items-start justify-start shadow-md w-[320px] text-base text-color dark:bg-shade-d500 relative border border-l pointer-events-auto', |
| type === 'success' && 'border-green-600 fill-green-500 bg-green-100 border-l-[3px]', |
| type === 'error' && 'border-red-600', |
| type === 'info' && '' |
| )} |
| > |
| <Icon |
| class={twMerge(type === 'success' && 'fill-green-500 ', 'stroke-green-100')} |
| name={icons[type]} |
| /> |
| |
| <button class="rounded-full p-1 absolute top-3 right-3" onclick={remove}> |
| <Icon name="close" class="w-[16px] h-[16px]" /> |
| </button> |
| |
| <div class="flex flex-col"> |
| <span> |
| {#if type !== 'info'} |
| <span class="font-semibold">{title[type]}</span> |
| {/if} |
| </span> |
| |
| <span class="text-sm">{description}</span> |
| </div> |
| </div> |
| </li> |
| {/each} |
| </ul> |
| </div> |