Add: New Toasts system

This commit is contained in:
Donatas Kirda 2024-05-20 12:19:42 +03:00
parent 9a3295adc4
commit 6a1788292a
Signed by: bloodwiing
GPG Key ID: 63020D8D3F4A164F
6 changed files with 332 additions and 7 deletions

198
src/comp/toast/toast.svelte Normal file
View File

@ -0,0 +1,198 @@
<script>
import Glowfx from "$comp/fx/glowfx.svelte";
import Tablericon from "$comp/tablericon.svelte";
import { removeToast } from "$lib/memory/toast";
import { onMount } from "svelte";
/**
* @type {import("$lib/memory/toast").Toast}
*/
export let toast;
/**
* @type {boolean}
*/
export let expand;
/**
* @type {number}
*/
export let id;
/** @type {HTMLElement} */
let element;
/** @type {number} */
let height;
/** @type {boolean | null} */
let inserted = null;
onMount(() => {
height = element.getBoundingClientRect().height;
inserted = true;
});
</script>
<style lang="scss">
.toast {
display: flex;
flex-flow: column nowrap;
gap: 8px;
position: absolute;
top: 0;
left: 0;
overflow: hidden;
width: max-content;
background: var(--gray-dim);
border-radius: 16px;
outline: 2px solid var(--background);
--translate-y: 0%;
--scale: 1.0;
--opacity: 0.0;
transform: translateY(var(--translate-y)) scale(var(--scale));
opacity: var(--opacity);
@keyframes appear {
0% {
opacity: 0.0;
transform: translateY(calc(var(--translate-y) + 50px)) scale(var(--scale));
}
100% {
opacity: var(--opacity);
transform: translateY(var(--translate-y)) scale(var(--scale));
}
}
@keyframes dismiss {
0% {
opacity: var(--opacity);
max-height: var(--height);
}
100% {
opacity: 0.0;
max-height: 0px;
}
}
animation: appear 0.2s 1, dismiss 2s forwards 1 var(--duration) ease-out;
&[data-inserted] {
position: relative;
&:nth-last-child(1) {
z-index: 0;
--translate-y: 0%;
--scale: 1.0;
--opacity: 1.0;
transform: translateY(0%) scale(1.0);
transition: 0.2s ease-out;
}
&:nth-last-child(2) {
z-index: -1;
--translate-y: 90%;
--scale: 0.9;
--opacity: 0.8;
transform: translateY(90%) scale(0.9);
transition: 0.2s ease-out;
}
&:nth-last-child(3) {
z-index: -2;
--translate-y: 190%;
--scale: 0.8;
--opacity: 0.6;
transform: translateY(190%) scale(0.8);
transition: 0.2s ease-out;
}
&:not([data-expand]):nth-last-child(n+4) {
max-height: 0;
opacity: 0;
--translate-y: 300%;
--scale: 0.7;
--opacity: 0.0;
transform: translateY(300%) scale(0.7);
}
&[data-expand]:nth-child(n+0) {
max-height: var(--height);
position: relative;
transform: unset;
opacity: 1.0;
left: 0;
opacity: 1;
--translate-y: 0%;
--opacity: 1.0;
--scale: 1.0;
transition: 0.2s ease-out;
}
}
&[data-type='error'] {
--toast-color: #e04343;
}
padding: 0px;
}
.content {
padding: 16px;
}
.bar {
position: absolute;
top: 0px;
left: 0px;
right: 0px;
height: 2px;
background: var(--toast-color);
@keyframes bar-animation {
from {
width: 100%;
}
to {
width: 0%;
}
}
&[data-animate] {
animation: bar-animation var(--duration) 1 forwards 0s linear;
}
}
.head {
display: flex;
flex-flow: row nowrap;
align-items: center;
gap: 8px;
h5 {
font-size: 1.1rem;
}
}
</style>
<button class="toast" bind:this={element} style="--height: {height}px; --duration: {(toast.timeout ?? 100000000) / 1000 - 2}s;" data-expand={expand ? true : null} data-inserted={inserted} data-type={toast.type} on:click={() => removeToast(id)}>
<Glowfx borderRadius={16}>
<div class="content">
{#if toast.timeout}
<div class="bar" style="--duration: {toast.timeout / 1000}s" data-animate></div>
{:else}
<div class="bar"></div>
{/if}
<div class="head">
<Tablericon name="bug" color="var(--toast-color)" size={24}></Tablericon>
<h5 class="typeTitle">{toast.title}</h5>
</div>
<span>{toast.message}</span>
</div>
</Glowfx>
</button>

View File

@ -0,0 +1,34 @@
<script>
import toasts from "$lib/memory/toast";
import Toast from "./toast.svelte";
/**
* @type {boolean}
*/
let expand;
</script>
<style lang="scss">
.toasts {
display: flex;
flex-flow: column nowrap;
align-items: end;
justify-content: end;
top: calc(100dvh - 16px);
transform: translateY(-100%);
width: max-content;
right: 16px;
gap: 16px;
overflow: hidden;
padding: 2px;
position: fixed;
z-index: 999;
}
</style>
<div role="none" class="toasts" on:mouseenter={() => {expand = true;}} on:mouseleave={() => {expand = false;}}>
{#each $toasts as toast (toast.id)}
<Toast id={toast.id} toast={toast} expand={expand}></Toast>
{/each}
</div>

82
src/lib/memory/toast.js Normal file
View File

@ -0,0 +1,82 @@
import { hashOf } from "$lib/client/hash";
import { writable } from "svelte/store";
/**
* @typedef {'error'} ToastType
* @typedef {number} ToastID
* @typedef {{
* type: ToastType,
* title: string,
* message: string,
* dismissCallback?: function(): void,
* timeout?: number | null
* }} Toast
*/
/**
* @type {import("svelte/store").Writable<(Toast & {id: ToastID})[]>}
*/
const toasts = writable([]);
export default toasts;
/**
* @param {Toast} toast
* @returns {ToastID}
*/
export function addToastObject(toast) {
/** @type {Toast & {id: ToastID}} */
const entry = {
id: hashOf(Date.now()),
...toast,
}
toasts.update((list) => {return [...list, entry];});
if (entry.timeout) {
setTimeout(() => {removeToast(entry.id);}, entry.timeout);
}
return entry.id;
}
/**
* @param {ToastType} type
* @param {string} title
* @param {string} message
* @param {function(): void} dismissCallback
* @param {number | undefined} [timeout=10000]
* @returns {ToastID}
*/
export function addToastCallback(type, title, message, dismissCallback, timeout = 10000) {
return addToastObject({
type: type,
title: title,
message: message,
dismissCallback: dismissCallback,
timeout: timeout,
});
}
/**
* @param {ToastType} type
* @param {string} title
* @param {string} message
* @param {number | undefined | null} [timeout=10000]
* @returns {ToastID}
*/
export function addToast(type, title, message, timeout = 10000) {
return addToastObject({
type: type,
title: title,
message: message,
timeout: timeout,
});
}
/**
* @param {number} id
*/
export function removeToast(id) {
toasts.update((list) => {return list.filter(x => x.id != id);});
}

View File

@ -9,13 +9,6 @@
* }} * }}
*/ */
export let data; export let data;
$: {
if ($page.form) {
console.log($page.form);
addToast('error', 'Test', 'cool!', null);
}
};
</script> </script>
<style lang="scss"> <style lang="scss">

17
src/routes/+layout.svelte Normal file
View File

@ -0,0 +1,17 @@
<script>
import ToastManager from "$comp/toast/toastmanager.svelte";
import { page } from "$app/stores";
import { addToast } from "$lib/memory/toast";
import { runIfError } from "$lib/status";
$: {
if ($page.form) {
runIfError($page.form, (error) => {
addToast('error', error.title, error.msg, 10000);
});
}
};
</script>
<ToastManager></ToastManager>
<slot />

1
static/icon/bug.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-bug"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 4a4 4 0 0 1 3.995 3.8l.005 .2a1 1 0 0 1 .428 .096l3.033 -1.938a1 1 0 1 1 1.078 1.684l-3.015 1.931a7.17 7.17 0 0 1 .476 2.227h3a1 1 0 0 1 0 2h-3v1a6.01 6.01 0 0 1 -.195 1.525l2.708 1.616a1 1 0 1 1 -1.026 1.718l-2.514 -1.501a6.002 6.002 0 0 1 -3.973 2.56v-5.918a1 1 0 0 0 -2 0v5.917a6.002 6.002 0 0 1 -3.973 -2.56l-2.514 1.503a1 1 0 1 1 -1.026 -1.718l2.708 -1.616a6.01 6.01 0 0 1 -.195 -1.526v-1h-3a1 1 0 0 1 0 -2h3.001v-.055a7 7 0 0 1 .474 -2.173l-3.014 -1.93a1 1 0 1 1 1.078 -1.684l3.032 1.939l.024 -.012l.068 -.027l.019 -.005l.016 -.006l.032 -.008l.04 -.013l.034 -.007l.034 -.004l.045 -.008l.015 -.001l.015 -.002l.087 -.004a4 4 0 0 1 4 -4zm0 2a2 2 0 0 0 -2 2h4a2 2 0 0 0 -2 -2z" /></svg>

After

Width:  |  Height:  |  Size: 933 B