atmosphere explorer
at main 106 lines 4.4 kB view raw
1import { createSignal, For, Show } from "solid-js"; 2import { createStore } from "solid-js/store"; 3 4export type Notification = { 5 id: string; 6 message: string; 7 progress?: number; 8 total?: number; 9 type?: "info" | "success" | "error"; 10 onCancel?: () => void; 11}; 12 13const [notifications, setNotifications] = createStore<Notification[]>([]); 14const [removingIds, setRemovingIds] = createSignal<Set<string>>(new Set()); 15 16export const addNotification = (notification: Omit<Notification, "id">) => { 17 const id = `notification-${Date.now()}-${Math.random()}`; 18 setNotifications(notifications.length, { ...notification, id }); 19 return id; 20}; 21 22export const updateNotification = (id: string, updates: Partial<Notification>) => { 23 setNotifications((n) => n.id === id, updates); 24}; 25 26export const removeNotification = (id: string) => { 27 setRemovingIds(new Set([...removingIds(), id])); 28 setTimeout(() => { 29 setNotifications((n) => n.filter((notification) => notification.id !== id)); 30 setRemovingIds((ids) => { 31 const newIds = new Set(ids); 32 newIds.delete(id); 33 return newIds; 34 }); 35 }, 250); 36}; 37 38export const NotificationContainer = () => { 39 return ( 40 <div class="pointer-events-none fixed bottom-4 left-4 z-60 flex flex-col gap-2"> 41 <For each={notifications}> 42 {(notification) => ( 43 <div 44 class="dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto flex min-w-64 flex-col gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 shadow-md select-none dark:border-neutral-700" 45 classList={{ 46 "border-blue-500 dark:border-blue-400": notification.type === "info", 47 "border-green-500 dark:border-green-400": notification.type === "success", 48 "border-red-500 dark:border-red-400": notification.type === "error", 49 "animate-[slideIn_0.25s_ease-in]": !removingIds().has(notification.id), 50 "animate-[slideOut_0.25s_ease-in]": removingIds().has(notification.id), 51 }} 52 onClick={() => 53 notification.progress === undefined && removeNotification(notification.id) 54 } 55 > 56 <div class="flex items-center gap-2 text-sm"> 57 <Show when={notification.progress !== undefined}> 58 <span class="iconify lucide--download" /> 59 </Show> 60 <Show when={notification.type === "success"}> 61 <span class="iconify lucide--check-circle text-green-600 dark:text-green-400" /> 62 </Show> 63 <Show when={notification.type === "error"}> 64 <span class="iconify lucide--x-circle text-red-500 dark:text-red-400" /> 65 </Show> 66 <span>{notification.message}</span> 67 </div> 68 <Show when={notification.progress !== undefined}> 69 <div class="flex flex-col gap-1"> 70 <Show 71 when={notification.total !== undefined && notification.total > 0} 72 fallback={ 73 <div class="text-xs text-neutral-600 dark:text-neutral-400"> 74 {notification.progress} MB 75 </div> 76 } 77 > 78 <div class="h-2 w-full overflow-hidden rounded-full bg-neutral-200 dark:bg-neutral-700"> 79 <div 80 class="h-full rounded-full bg-blue-500 transition-all dark:bg-blue-400" 81 style={{ width: `${notification.progress}%` }} 82 /> 83 </div> 84 <div class="text-xs text-neutral-600 dark:text-neutral-400"> 85 {notification.progress}% 86 </div> 87 </Show> 88 <Show when={notification.onCancel}> 89 <button 90 class="dark:hover:bg-dark-200 dark:active:bg-dark-100 dark:bg-dark-300 mt-1 rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700" 91 onClick={(e) => { 92 e.stopPropagation(); 93 notification.onCancel?.(); 94 }} 95 > 96 Cancel 97 </button> 98 </Show> 99 </div> 100 </Show> 101 </div> 102 )} 103 </For> 104 </div> 105 ); 106};