diff --git a/packages/app/src/components/toast.ts b/packages/app/src/components/toast.ts new file mode 100644 index 0000000000..1ca6fdcfe7 --- /dev/null +++ b/packages/app/src/components/toast.ts @@ -0,0 +1,104 @@ +// Copyright: https://github.com/toeverything/blocksuite/commit/8032ef3ab97aefce01664b36502fc392c5db8b78#diff-bf5b41be21936f9165a8400c7f20e24d3dbc49644ba57b9258e0943f0dc1c464 +import { css, html, TemplateResult } from 'lit'; + +export const sleep = (ms = 0) => + new Promise(resolve => setTimeout(resolve, ms)); + +let ToastContainer: HTMLDivElement | null = null; + +/** + * DO NOT USE FOR USER INPUT + * See https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518 + */ +const htmlToElement = (html: string | TemplateResult) => { + const template = document.createElement('template'); + if (typeof html === 'string') { + html = html.trim(); // Never return a text node of whitespace as the result + template.innerHTML = html; + } else { + const { strings, values } = html; + const v = [...values, '']; // + last emtpty part + template.innerHTML = strings.reduce((acc, cur, i) => acc + cur + v[i], ''); + } + return template.content.firstChild as T; +}; + +const createToastContainer = () => { + const styles = css` + position: fixed; + z-index: 9999; + top: 16px; + left: 16px; + right: 16px; + bottom: 78px; + pointer-events: none; + display: flex; + flex-direction: column-reverse; + align-items: center; + `; + const template = html`
`; + const element = htmlToElement(template); + document.body.appendChild(element); + return element; +}; + +/** + * @example + * ```ts + * toast('Hello World'); + * ``` + */ +export const toast = (message: string, duration = 2500) => { + if (!ToastContainer) { + ToastContainer = createToastContainer(); + } + + const styles = css` + max-width: 480px; + text-align: center; + font-family: var(--affine-font-family); + font-size: var(--affine-font-sm); + padding: 6px 12px; + margin: 10px 0 0 0; + color: var(--affine-tooltip-color); + background: var(--affine-tooltip-background); + box-shadow: var(--affine-tooltip-shadow); + border-radius: 10px; + transition: all 230ms cubic-bezier(0.21, 1.02, 0.73, 1); + opacity: 0; + `; + + const template = html`
`; + const element = htmlToElement(template); + // message is not trusted + element.textContent = message; + ToastContainer.appendChild(element); + + const fadeIn = [ + { + opacity: 0, + }, + { opacity: 1 }, + ]; + const options = { + duration: 230, + easing: 'cubic-bezier(0.21, 1.02, 0.73, 1)', + fill: 'forwards' as const, + }; // satisfies KeyframeAnimationOptions; + element.animate(fadeIn, options); + + setTimeout(async () => { + const fadeOut = fadeIn.reverse(); + const animation = element.animate(fadeOut, options); + await animation.finished; + element.style.maxHeight = '0'; + element.style.margin = '0'; + element.style.padding = '0'; + // wait for transition + await sleep(230); + element.remove(); + }, duration); + return element; +}; + +export default toast; diff --git a/packages/app/src/pages/affine.tsx b/packages/app/src/pages/affine.tsx index a486a254d0..89609d3ada 100644 --- a/packages/app/src/pages/affine.tsx +++ b/packages/app/src/pages/affine.tsx @@ -5,6 +5,7 @@ import Modal from '@/ui/modal'; import { useState } from 'react'; import { Button } from '@/ui/button'; import { FavouritedIcon } from '@blocksuite/icons'; +import { toast } from '@/components/toast'; export const StyledHeader = styled('div')({ height: '60px', width: '100vw', @@ -38,7 +39,14 @@ const Affine = () => { - +