Add: New Toasts system
This commit is contained in:
parent
9a3295adc4
commit
6a1788292a
198
src/comp/toast/toast.svelte
Normal file
198
src/comp/toast/toast.svelte
Normal 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>
|
||||
34
src/comp/toast/toastmanager.svelte
Normal file
34
src/comp/toast/toastmanager.svelte
Normal 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
82
src/lib/memory/toast.js
Normal 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);});
|
||||
}
|
||||
@ -9,13 +9,6 @@
|
||||
* }}
|
||||
*/
|
||||
export let data;
|
||||
|
||||
$: {
|
||||
if ($page.form) {
|
||||
console.log($page.form);
|
||||
addToast('error', 'Test', 'cool!', null);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
17
src/routes/+layout.svelte
Normal file
17
src/routes/+layout.svelte
Normal 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
1
static/icon/bug.svg
Normal 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 |
Loading…
x
Reference in New Issue
Block a user