diff --git a/jsr.jsonc b/jsr.jsonc new file mode 100644 index 0000000..d35fad7 --- /dev/null +++ b/jsr.jsonc @@ -0,0 +1,24 @@ +{ + "name": "@bureaudouble/sonner", + "version": "0.0.2", + "compilerOptions": { + "jsx": "react-jsx", + "lib": [ + "es2022", + "dom", + "dom.iterable", + "dom.asynciterable" + ] + }, + "exclude": ["**/*", "!README.md", "!LICENSE.md", "!src", "!jsr.jsonc"], + "imports": { + "react": "npm:react@^18.2.0", + "react-dom": "npm:react-dom@^18.2.0", + "amuchina": "npm:amuchina@^1.0.12", + "react-style-singleton": "jsr:@bureaudouble-forks/react-style-singleton@^0.0.1" + }, + "exports": { + ".": "./src/index.tsx" + } +} + \ No newline at end of file diff --git a/package.json b/package.json index 63debd7..585c04d 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "@types/dompurify": "^3.0.5", "@types/node": "^18.11.13", "@types/react": "^18.0.26", - "dompurify": "^3.0.9", + "amuchina": "^1.0.12", + "react-style-singleton": "^2.2.1", "prettier": "^2.8.4", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/assets.tsx b/src/assets.tsx index 328362d..d98ee04 100644 --- a/src/assets.tsx +++ b/src/assets.tsx @@ -1,6 +1,7 @@ 'use client'; +// @deno-types="npm:@types/react@^18.2.0" import React from 'react'; -import type { ToastTypes } from './types'; +import type { ToastTypes } from './types.ts'; export const getAsset = (type: ToastTypes): JSX.Element | null => { switch (type) { @@ -35,7 +36,7 @@ export const Loader = ({ visible }: { visible: boolean }) => { ); }; -const SuccessIcon = ( +const SuccessIcon: React.JSX.Element = ( ); -const WarningIcon = ( +const WarningIcon: React.JSX.Element = ( ); -const InfoIcon = ( +const InfoIcon: React.JSX.Element = ( ); -const ErrorIcon = ( +const ErrorIcon: React.JSX.Element = ( { - const [isDocumentHidden, setIsDocumentHidden] = React.useState(false); +export const useIsDocumentHidden = (): boolean => { + const [isDocumentHidden, setIsDocumentHidden] = React.useState(false); React.useEffect(() => { const callback = () => { diff --git a/src/index.tsx b/src/index.tsx index 4260166..aa0b34c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,16 @@ 'use client'; +// @deno-types="npm:@types/react@^18.2.0" import React from 'react'; +// @deno-types="npm:@types/react-dom@^18.2.0" import ReactDOM from 'react-dom'; -import DOMPurify from 'dompurify'; -import { getAsset, Loader } from './assets'; -import { useIsDocumentHidden } from './hooks'; -import { toast, ToastState } from './state'; -import './styles.css'; +import { styleHookSingleton } from 'react-style-singleton'; +import Amuchina from 'amuchina'; +import { getAsset, Loader } from './assets.tsx'; +import { useIsDocumentHidden } from './hooks.tsx'; +import { toast, ToastState } from './state.ts'; +import styles from "./styles.ts"; import { isAction, type ExternalToast, @@ -16,7 +19,7 @@ import { type ToastProps, type ToastT, type ToastToDismiss, -} from './types'; +} from './types.ts'; // Visible toasts amount const VISIBLE_TOASTS_AMOUNT = 3; @@ -43,7 +46,9 @@ function _cn(...classes: (string | undefined)[]) { return classes.filter(Boolean).join(' '); } -const Toast = (props: ToastProps) => { +const useStyle: ReturnType = styleHookSingleton(); +const Toast = (props: ToastProps): React.JSX.Element => { + useStyle(styles); const { invert: ToasterInvert, toast, @@ -250,7 +255,9 @@ const Toast = (props: ToastProps) => { } function sanitizeHTML(html: string): { __html: string } { - return { __html: DOMPurify.sanitize(html) }; + const amuchina = globalThis.document ? new Amuchina() : null; + const parser = amuchina ? new DOMParser() : null; + return { __html: amuchina?.sanitize(parser?.parseFromString(html, 'text/html'))?.documentElement.innerHTML ?? "" }; } return ( @@ -467,7 +474,7 @@ function getDocumentDirection(): ToasterProps['dir'] { return dirAttribute as ToasterProps['dir']; } -const Toaster = (props: ToasterProps) => { +const Toaster = (props: ToasterProps): React.JSX.Element => { const { invert, position = 'bottom-right', diff --git a/src/state.ts b/src/state.ts index 03f7915..677b717 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,5 +1,6 @@ +// @deno-types="npm:@types/react@^18.2.0" import React from 'react'; -import type { ExternalToast, ToastT, PromiseData, PromiseT, ToastToDismiss, ToastTypes } from './types'; +import type { ExternalToast, ToastT, PromiseData, PromiseT, ToastToDismiss, ToastTypes } from './types.ts'; let toastsCounter = 1; @@ -13,7 +14,7 @@ class Observer { } // We use arrow functions to maintain the correct `this` reference - subscribe = (subscriber: (toast: ToastT | ToastToDismiss) => void) => { + subscribe = (subscriber: (toast: ToastT | ToastToDismiss) => void): () => void => { this.subscribers.push(subscriber); return () => { @@ -22,11 +23,11 @@ class Observer { }; }; - publish = (data: ToastT) => { + publish = (data: ToastT): void => { this.subscribers.forEach((subscriber) => subscriber(data)); }; - addToast = (data: ToastT) => { + addToast = (data: ToastT): void => { this.publish(data); this.toasts = [...this.toasts, data]; }; @@ -38,7 +39,7 @@ class Observer { promise?: PromiseT; jsx?: React.ReactElement; }, - ) => { + ): string | number => { const { message, ...rest } = data; const id = typeof data?.id === 'number' || data.id?.length > 0 ? data.id : toastsCounter++; const alreadyExists = this.toasts.find((toast) => { @@ -68,7 +69,7 @@ class Observer { return id; }; - dismiss = (id?: number | string) => { + dismiss = (id?: number | string): string | number => { if (!id) { this.toasts.forEach((toast) => { this.subscribers.forEach((subscriber) => subscriber({ id: toast.id, dismiss: true })); @@ -79,31 +80,31 @@ class Observer { return id; }; - message = (message: string | React.ReactNode, data?: ExternalToast) => { + message = (message: string | React.ReactNode, data?: ExternalToast): string | number => { return this.create({ ...data, message }); }; - error = (message: string | React.ReactNode, data?: ExternalToast) => { + error = (message: string | React.ReactNode, data?: ExternalToast): string | number => { return this.create({ ...data, message, type: 'error' }); }; - success = (message: string | React.ReactNode, data?: ExternalToast) => { + success = (message: string | React.ReactNode, data?: ExternalToast): string | number => { return this.create({ ...data, type: 'success', message }); }; - info = (message: string | React.ReactNode, data?: ExternalToast) => { + info = (message: string | React.ReactNode, data?: ExternalToast): string | number => { return this.create({ ...data, type: 'info', message }); }; - warning = (message: string | React.ReactNode, data?: ExternalToast) => { + warning = (message: string | React.ReactNode, data?: ExternalToast): string | number => { return this.create({ ...data, type: 'warning', message }); }; - loading = (message: string | React.ReactNode, data?: ExternalToast) => { + loading = (message: string | React.ReactNode, data?: ExternalToast): string | number => { return this.create({ ...data, type: 'loading', message }); }; - promise = (promise: PromiseT, data?: PromiseData) => { + promise = (promise: PromiseT, data?: PromiseData): string | number => { if (!data) { // Nothing to show return; @@ -166,17 +167,17 @@ class Observer { return id; }; - custom = (jsx: (id: number | string) => React.ReactElement, data?: ExternalToast) => { + custom = (jsx: (id: number | string) => React.ReactElement, data?: ExternalToast): string | number => { const id = data?.id || toastsCounter++; this.create({ jsx: jsx(id), id, ...data }); return id; }; } -export const ToastState = new Observer(); +export const ToastState: Observer = new Observer(); // bind this to the toast function -const toastFunction = (message: string | React.ReactNode, data?: ExternalToast) => { +const toastFunction = (message: string | React.ReactNode, data?: ExternalToast): string | number => { const id = data?.id || toastsCounter++; ToastState.addToast({ @@ -187,10 +188,20 @@ const toastFunction = (message: string | React.ReactNode, data?: ExternalToast) return id; }; -const basicToast = toastFunction; +const basicToast: typeof toastFunction = toastFunction; // We use `Object.assign` to maintain the correct types as we would lose them otherwise -export const toast = Object.assign(basicToast, { +export const toast: ((message: string | React.ReactNode, data?: ExternalToast) => string | number) & { + success: typeof ToastState.success, + info: typeof ToastState.info, + warning: typeof ToastState.warning, + error: typeof ToastState.error, + custom: typeof ToastState.custom, + message: typeof ToastState.message, + promise: typeof ToastState.promise, + dismiss: typeof ToastState.dismiss, + loading: typeof ToastState.loading, +} = Object.assign(basicToast, { success: ToastState.success, info: ToastState.info, warning: ToastState.warning, diff --git a/src/styles.css b/src/styles.css deleted file mode 100644 index 656f81d..0000000 --- a/src/styles.css +++ /dev/null @@ -1,634 +0,0 @@ -:where(html[dir='ltr']), -:where([data-sonner-toaster][dir='ltr']) { - --toast-icon-margin-start: -3px; - --toast-icon-margin-end: 4px; - --toast-svg-margin-start: -1px; - --toast-svg-margin-end: 0px; - --toast-button-margin-start: auto; - --toast-button-margin-end: 0; - --toast-close-button-start: 0; - --toast-close-button-end: unset; - --toast-close-button-transform: translate(-35%, -35%); -} - -:where(html[dir='rtl']), -:where([data-sonner-toaster][dir='rtl']) { - --toast-icon-margin-start: 4px; - --toast-icon-margin-end: -3px; - --toast-svg-margin-start: 0px; - --toast-svg-margin-end: -1px; - --toast-button-margin-start: 0; - --toast-button-margin-end: auto; - --toast-close-button-start: unset; - --toast-close-button-end: 0; - --toast-close-button-transform: translate(35%, -35%); -} - -:where([data-sonner-toaster]) { - position: fixed; - width: var(--width); - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, - Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; - --gray1: hsl(0, 0%, 99%); - --gray2: hsl(0, 0%, 97.3%); - --gray3: hsl(0, 0%, 95.1%); - --gray4: hsl(0, 0%, 93%); - --gray5: hsl(0, 0%, 90.9%); - --gray6: hsl(0, 0%, 88.7%); - --gray7: hsl(0, 0%, 85.8%); - --gray8: hsl(0, 0%, 78%); - --gray9: hsl(0, 0%, 56.1%); - --gray10: hsl(0, 0%, 52.3%); - --gray11: hsl(0, 0%, 43.5%); - --gray12: hsl(0, 0%, 9%); - --border-radius: 8px; - box-sizing: border-box; - padding: 0; - margin: 0; - list-style: none; - outline: none; - z-index: 999999999; -} - -:where([data-sonner-toaster][data-x-position='right']) { - right: max(var(--offset), env(safe-area-inset-right)); -} - -:where([data-sonner-toaster][data-x-position='left']) { - left: max(var(--offset), env(safe-area-inset-left)); -} - -:where([data-sonner-toaster][data-x-position='center']) { - left: 50%; - transform: translateX(-50%); -} - -:where([data-sonner-toaster][data-y-position='top']) { - top: max(var(--offset), env(safe-area-inset-top)); -} - -:where([data-sonner-toaster][data-y-position='bottom']) { - bottom: max(var(--offset), env(safe-area-inset-bottom)); -} - -:where([data-sonner-toast]) { - --y: translateY(100%); - --lift-amount: calc(var(--lift) * var(--gap)); - z-index: var(--z-index); - position: absolute; - opacity: 0; - transform: var(--y); - filter: blur(0); - /* https://stackoverflow.com/questions/48124372/pointermove-event-not-working-with-touch-why-not */ - touch-action: none; - transition: transform 400ms, opacity 400ms, height 400ms, box-shadow 200ms; - box-sizing: border-box; - outline: none; - overflow-wrap: anywhere; -} - -:where([data-sonner-toast][data-styled='true']) { - padding: 16px; - background: var(--normal-bg); - border: 1px solid var(--normal-border); - color: var(--normal-text); - border-radius: var(--border-radius); - box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); - width: var(--width); - font-size: 13px; - display: flex; - align-items: center; - gap: 6px; -} - -:where([data-sonner-toast]:focus-visible) { - box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2); -} - -:where([data-sonner-toast][data-y-position='top']) { - top: 0; - --y: translateY(-100%); - --lift: 1; - --lift-amount: calc(1 * var(--gap)); -} - -:where([data-sonner-toast][data-y-position='bottom']) { - bottom: 0; - --y: translateY(100%); - --lift: -1; - --lift-amount: calc(var(--lift) * var(--gap)); -} - -:where([data-sonner-toast]) :where([data-description]) { - font-weight: 400; - line-height: 1.4; - color: inherit; -} - -:where([data-sonner-toast]) :where([data-title]) { - font-weight: 500; - line-height: 1.5; - color: inherit; -} - -:where([data-sonner-toast]) :where([data-icon]) { - display: flex; - height: 16px; - width: 16px; - position: relative; - justify-content: flex-start; - align-items: center; - flex-shrink: 0; - margin-left: var(--toast-icon-margin-start); - margin-right: var(--toast-icon-margin-end); -} - -:where([data-sonner-toast][data-promise='true']) :where([data-icon]) > svg { - opacity: 0; - transform: scale(0.8); - transform-origin: center; - animation: sonner-fade-in 300ms ease forwards; -} - -:where([data-sonner-toast]) :where([data-icon]) > * { - flex-shrink: 0; -} - -:where([data-sonner-toast]) :where([data-icon]) svg { - margin-left: var(--toast-svg-margin-start); - margin-right: var(--toast-svg-margin-end); -} - -:where([data-sonner-toast]) :where([data-content]) { - display: flex; - flex-direction: column; - gap: 2px; -} - -[data-sonner-toast][data-styled="true"] [data-button] { - border-radius: 4px; - padding-left: 8px; - padding-right: 8px; - height: 24px; - font-size: 12px; - color: var(--normal-bg); - background: var(--normal-text); - margin-left: var(--toast-button-margin-start); - margin-right: var(--toast-button-margin-end); - border: none; - cursor: pointer; - outline: none; - display: flex; - align-items: center; - flex-shrink: 0; - transition: opacity 400ms, box-shadow 200ms; -} - -:where([data-sonner-toast]) :where([data-button]):focus-visible { - box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4); -} - -:where([data-sonner-toast]) :where([data-button]):first-of-type { - margin-left: var(--toast-button-margin-start); - margin-right: var(--toast-button-margin-end); -} - -:where([data-sonner-toast]) :where([data-cancel]) { - color: var(--normal-text); - background: rgba(0, 0, 0, 0.08); -} - -:where([data-sonner-toast][data-theme='dark']) :where([data-cancel]) { - background: rgba(255, 255, 255, 0.3); -} - -:where([data-sonner-toast]) :where([data-close-button]) { - position: absolute; - left: var(--toast-close-button-start); - right: var(--toast-close-button-end); - top: 0; - height: 20px; - width: 20px; - display: flex; - justify-content: center; - align-items: center; - padding: 0; - background: var(--gray1); - color: var(--gray12); - border: 1px solid var(--gray4); - transform: var(--toast-close-button-transform); - border-radius: 50%; - cursor: pointer; - z-index: 1; - transition: opacity 100ms, background 200ms, border-color 200ms; -} - -:where([data-sonner-toast]) :where([data-close-button]):focus-visible { - box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2); -} - -:where([data-sonner-toast]) :where([data-disabled='true']) { - cursor: not-allowed; -} - -:where([data-sonner-toast]):hover :where([data-close-button]):hover { - background: var(--gray2); - border-color: var(--gray5); -} - -/* Leave a ghost div to avoid setting hover to false when swiping out */ -:where([data-sonner-toast][data-swiping='true'])::before { - content: ''; - position: absolute; - left: 0; - right: 0; - height: 100%; - z-index: -1; -} - -:where([data-sonner-toast][data-y-position='top'][data-swiping='true'])::before { - /* y 50% needed to distribute height additional height evenly */ - bottom: 50%; - transform: scaleY(3) translateY(50%); -} - -:where([data-sonner-toast][data-y-position='bottom'][data-swiping='true'])::before { - /* y -50% needed to distribute height additional height evenly */ - top: 50%; - transform: scaleY(3) translateY(-50%); -} - -/* Leave a ghost div to avoid setting hover to false when transitioning out */ -:where([data-sonner-toast][data-swiping='false'][data-removed='true'])::before { - content: ''; - position: absolute; - inset: 0; - transform: scaleY(2); -} - -/* Needed to avoid setting hover to false when inbetween toasts */ -:where([data-sonner-toast])::after { - content: ''; - position: absolute; - left: 0; - height: calc(var(--gap) + 1px); - bottom: 100%; - width: 100%; -} - -:where([data-sonner-toast][data-mounted='true']) { - --y: translateY(0); - opacity: 1; -} - -:where([data-sonner-toast][data-expanded='false'][data-front='false']) { - --scale: var(--toasts-before) * 0.05 + 1; - --y: translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale))); - height: var(--front-toast-height); -} - -:where([data-sonner-toast]) > * { - transition: opacity 400ms; -} - -:where([data-sonner-toast][data-expanded='false'][data-front='false'][data-styled='true']) > * { - opacity: 0; -} - -:where([data-sonner-toast][data-visible='false']) { - opacity: 0; - pointer-events: none; -} - -:where([data-sonner-toast][data-mounted='true'][data-expanded='true']) { - --y: translateY(calc(var(--lift) * var(--offset))); - height: var(--initial-height); -} - -:where([data-sonner-toast][data-removed='true'][data-front='true'][data-swipe-out='false']) { - --y: translateY(calc(var(--lift) * -100%)); - opacity: 0; -} - -:where([data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='true']) { - --y: translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%)); - opacity: 0; -} - -:where([data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='false']) { - --y: translateY(40%); - opacity: 0; - transition: transform 500ms, opacity 200ms; -} - -/* Bump up the height to make sure hover state doesn't get set to false */ -:where([data-sonner-toast][data-removed='true'][data-front='false'])::before { - height: calc(var(--initial-height) + 20%); -} - -[data-sonner-toast][data-swiping='true'] { - transform: var(--y) translateY(var(--swipe-amount, 0px)); - transition: none; -} - -[data-sonner-toast][data-swipe-out='true'][data-y-position='bottom'], -[data-sonner-toast][data-swipe-out='true'][data-y-position='top'] { - animation: swipe-out 200ms ease-out forwards; -} - -@keyframes swipe-out { - from { - transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount))); - opacity: 1; - } - - to { - transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount) + var(--lift) * -100%)); - opacity: 0; - } -} - -@media (max-width: 600px) { - [data-sonner-toaster] { - position: fixed; - --mobile-offset: 16px; - right: var(--mobile-offset); - left: var(--mobile-offset); - width: 100%; - } - - [data-sonner-toaster] [data-sonner-toast] { - left: 0; - right: 0; - width: calc(100% - var(--mobile-offset) * 2); - } - - [data-sonner-toaster][data-x-position='left'] { - left: var(--mobile-offset); - } - - [data-sonner-toaster][data-y-position='bottom'] { - bottom: 20px; - } - - [data-sonner-toaster][data-y-position='top'] { - top: 20px; - } - - [data-sonner-toaster][data-x-position='center'] { - left: var(--mobile-offset); - right: var(--mobile-offset); - transform: none; - } -} - -[data-sonner-toaster][data-theme='light'] { - --normal-bg: #fff; - --normal-border: var(--gray4); - --normal-text: var(--gray12); - - --success-bg: hsl(143, 85%, 96%); - --success-border: hsl(145, 92%, 91%); - --success-text: hsl(140, 100%, 27%); - - --info-bg: hsl(208, 100%, 97%); - --info-border: hsl(221, 91%, 91%); - --info-text: hsl(210, 92%, 45%); - - --warning-bg: hsl(49, 100%, 97%); - --warning-border: hsl(49, 91%, 91%); - --warning-text: hsl(31, 92%, 45%); - - --error-bg: hsl(359, 100%, 97%); - --error-border: hsl(359, 100%, 94%); - --error-text: hsl(360, 100%, 45%); -} - -[data-sonner-toaster][data-theme='light'] [data-sonner-toast][data-invert='true'] { - --normal-bg: #000; - --normal-border: hsl(0, 0%, 20%); - --normal-text: var(--gray1); -} - -[data-sonner-toaster][data-theme='dark'] [data-sonner-toast][data-invert='true'] { - --normal-bg: #fff; - --normal-border: var(--gray3); - --normal-text: var(--gray12); -} - -[data-sonner-toaster][data-theme='dark'] { - --normal-bg: #000; - --normal-border: hsl(0, 0%, 20%); - --normal-text: var(--gray1); - - --success-bg: hsl(150, 100%, 6%); - --success-border: hsl(147, 100%, 12%); - --success-text: hsl(150, 86%, 65%); - - --info-bg: hsl(215, 100%, 6%); - --info-border: hsl(223, 100%, 12%); - --info-text: hsl(216, 87%, 65%); - - --warning-bg: hsl(64, 100%, 6%); - --warning-border: hsl(60, 100%, 12%); - --warning-text: hsl(46, 87%, 65%); - - --error-bg: hsl(358, 76%, 10%); - --error-border: hsl(357, 89%, 16%); - --error-text: hsl(358, 100%, 81%); -} - -[data-rich-colors='true'] [data-sonner-toast][data-type='success'] { - background: var(--success-bg); - border-color: var(--success-border); - color: var(--success-text); -} - -[data-rich-colors='true'] [data-sonner-toast][data-type='success'] [data-close-button] { - background: var(--success-bg); - border-color: var(--success-border); - color: var(--success-text); -} - -[data-rich-colors='true'] [data-sonner-toast][data-type='info'] { - background: var(--info-bg); - border-color: var(--info-border); - color: var(--info-text); -} - -[data-rich-colors='true'] [data-sonner-toast][data-type='info'] [data-close-button] { - background: var(--info-bg); - border-color: var(--info-border); - color: var(--info-text); -} - -[data-rich-colors='true'] [data-sonner-toast][data-type='warning'] { - background: var(--warning-bg); - border-color: var(--warning-border); - color: var(--warning-text); -} - -[data-rich-colors='true'] [data-sonner-toast][data-type='warning'] [data-close-button] { - background: var(--warning-bg); - border-color: var(--warning-border); - color: var(--warning-text); -} - -[data-rich-colors='true'] [data-sonner-toast][data-type='error'] { - background: var(--error-bg); - border-color: var(--error-border); - color: var(--error-text); -} - -[data-rich-colors='true'] [data-sonner-toast][data-type='error'] [data-close-button] { - background: var(--error-bg); - border-color: var(--error-border); - color: var(--error-text); -} - -.sonner-loading-wrapper { - --size: 16px; - height: var(--size); - width: var(--size); - position: absolute; - inset: 0; - z-index: 10; -} - -.sonner-loading-wrapper[data-visible='false'] { - transform-origin: center; - animation: sonner-fade-out 0.2s ease forwards; -} - -.sonner-spinner { - position: relative; - top: 50%; - left: 50%; - height: var(--size); - width: var(--size); -} - -.sonner-loading-bar { - animation: sonner-spin 1.2s linear infinite; - background: var(--gray11); - border-radius: 6px; - height: 8%; - left: -10%; - position: absolute; - top: -3.9%; - width: 24%; -} - -.sonner-loading-bar:nth-child(1) { - animation-delay: -1.2s; - transform: rotate(0.0001deg) translate(146%); -} - -.sonner-loading-bar:nth-child(2) { - animation-delay: -1.1s; - transform: rotate(30deg) translate(146%); -} - -.sonner-loading-bar:nth-child(3) { - animation-delay: -1s; - transform: rotate(60deg) translate(146%); -} - -.sonner-loading-bar:nth-child(4) { - animation-delay: -0.9s; - transform: rotate(90deg) translate(146%); -} - -.sonner-loading-bar:nth-child(5) { - animation-delay: -0.8s; - transform: rotate(120deg) translate(146%); -} - -.sonner-loading-bar:nth-child(6) { - animation-delay: -0.7s; - transform: rotate(150deg) translate(146%); -} - -.sonner-loading-bar:nth-child(7) { - animation-delay: -0.6s; - transform: rotate(180deg) translate(146%); -} - -.sonner-loading-bar:nth-child(8) { - animation-delay: -0.5s; - transform: rotate(210deg) translate(146%); -} - -.sonner-loading-bar:nth-child(9) { - animation-delay: -0.4s; - transform: rotate(240deg) translate(146%); -} - -.sonner-loading-bar:nth-child(10) { - animation-delay: -0.3s; - transform: rotate(270deg) translate(146%); -} - -.sonner-loading-bar:nth-child(11) { - animation-delay: -0.2s; - transform: rotate(300deg) translate(146%); -} - -.sonner-loading-bar:nth-child(12) { - animation-delay: -0.1s; - transform: rotate(330deg) translate(146%); -} - -@keyframes sonner-fade-in { - 0% { - opacity: 0; - transform: scale(0.8); - } - 100% { - opacity: 1; - transform: scale(1); - } -} - -@keyframes sonner-fade-out { - 0% { - opacity: 1; - transform: scale(1); - } - 100% { - opacity: 0; - transform: scale(0.8); - } -} - -@keyframes sonner-spin { - 0% { - opacity: 1; - } - 100% { - opacity: 0.15; - } -} - -@media (prefers-reduced-motion) { - [data-sonner-toast], - [data-sonner-toast] > *, - .sonner-loading-bar { - transition: none !important; - animation: none !important; - } -} - -.sonner-loader { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - transform-origin: center; - transition: opacity 200ms, transform 200ms; -} - -.sonner-loader[data-visible='false'] { - opacity: 0; - transform: scale(0.8) translate(-50%, -50%); -} diff --git a/src/styles.ts b/src/styles.ts new file mode 100644 index 0000000..8dfa7b4 --- /dev/null +++ b/src/styles.ts @@ -0,0 +1,638 @@ +const styles: string = /* css */` + :where(html[dir='ltr']), + :where([data-sonner-toaster][dir='ltr']) { + --toast-icon-margin-start: -3px; + --toast-icon-margin-end: 4px; + --toast-svg-margin-start: -1px; + --toast-svg-margin-end: 0px; + --toast-button-margin-start: auto; + --toast-button-margin-end: 0; + --toast-close-button-start: 0; + --toast-close-button-end: unset; + --toast-close-button-transform: translate(-35%, -35%); + } + + :where(html[dir='rtl']), + :where([data-sonner-toaster][dir='rtl']) { + --toast-icon-margin-start: 4px; + --toast-icon-margin-end: -3px; + --toast-svg-margin-start: 0px; + --toast-svg-margin-end: -1px; + --toast-button-margin-start: 0; + --toast-button-margin-end: auto; + --toast-close-button-start: unset; + --toast-close-button-end: 0; + --toast-close-button-transform: translate(35%, -35%); + } + + :where([data-sonner-toaster]) { + position: fixed; + width: var(--width); + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, + Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + --gray1: hsl(0, 0%, 99%); + --gray2: hsl(0, 0%, 97.3%); + --gray3: hsl(0, 0%, 95.1%); + --gray4: hsl(0, 0%, 93%); + --gray5: hsl(0, 0%, 90.9%); + --gray6: hsl(0, 0%, 88.7%); + --gray7: hsl(0, 0%, 85.8%); + --gray8: hsl(0, 0%, 78%); + --gray9: hsl(0, 0%, 56.1%); + --gray10: hsl(0, 0%, 52.3%); + --gray11: hsl(0, 0%, 43.5%); + --gray12: hsl(0, 0%, 9%); + --border-radius: 8px; + box-sizing: border-box; + padding: 0; + margin: 0; + list-style: none; + outline: none; + z-index: 999999999; + } + + :where([data-sonner-toaster][data-x-position='right']) { + right: max(var(--offset), env(safe-area-inset-right)); + } + + :where([data-sonner-toaster][data-x-position='left']) { + left: max(var(--offset), env(safe-area-inset-left)); + } + + :where([data-sonner-toaster][data-x-position='center']) { + left: 50%; + transform: translateX(-50%); + } + + :where([data-sonner-toaster][data-y-position='top']) { + top: max(var(--offset), env(safe-area-inset-top)); + } + + :where([data-sonner-toaster][data-y-position='bottom']) { + bottom: max(var(--offset), env(safe-area-inset-bottom)); + } + + :where([data-sonner-toast]) { + --y: translateY(100%); + --lift-amount: calc(var(--lift) * var(--gap)); + z-index: var(--z-index); + position: absolute; + opacity: 0; + transform: var(--y); + filter: blur(0); + /* https://stackoverflow.com/questions/48124372/pointermove-event-not-working-with-touch-why-not */ + touch-action: none; + transition: transform 400ms, opacity 400ms, height 400ms, box-shadow 200ms; + box-sizing: border-box; + outline: none; + overflow-wrap: anywhere; + } + + :where([data-sonner-toast][data-styled='true']) { + padding: 16px; + background: var(--normal-bg); + border: 1px solid var(--normal-border); + color: var(--normal-text); + border-radius: var(--border-radius); + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); + width: var(--width); + font-size: 13px; + display: flex; + align-items: center; + gap: 6px; + } + + :where([data-sonner-toast]:focus-visible) { + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2); + } + + :where([data-sonner-toast][data-y-position='top']) { + top: 0; + --y: translateY(-100%); + --lift: 1; + --lift-amount: calc(1 * var(--gap)); + } + + :where([data-sonner-toast][data-y-position='bottom']) { + bottom: 0; + --y: translateY(100%); + --lift: -1; + --lift-amount: calc(var(--lift) * var(--gap)); + } + + :where([data-sonner-toast]) :where([data-description]) { + font-weight: 400; + line-height: 1.4; + color: inherit; + } + + :where([data-sonner-toast]) :where([data-title]) { + font-weight: 500; + line-height: 1.5; + color: inherit; + } + + :where([data-sonner-toast]) :where([data-icon]) { + display: flex; + height: 16px; + width: 16px; + position: relative; + justify-content: flex-start; + align-items: center; + flex-shrink: 0; + margin-left: var(--toast-icon-margin-start); + margin-right: var(--toast-icon-margin-end); + } + + :where([data-sonner-toast][data-promise='true']) :where([data-icon]) > svg { + opacity: 0; + transform: scale(0.8); + transform-origin: center; + animation: sonner-fade-in 300ms ease forwards; + } + + :where([data-sonner-toast]) :where([data-icon]) > * { + flex-shrink: 0; + } + + :where([data-sonner-toast]) :where([data-icon]) svg { + margin-left: var(--toast-svg-margin-start); + margin-right: var(--toast-svg-margin-end); + } + + :where([data-sonner-toast]) :where([data-content]) { + display: flex; + flex-direction: column; + gap: 2px; + } + + [data-sonner-toast][data-styled="true"] [data-button] { + border-radius: 4px; + padding-left: 8px; + padding-right: 8px; + height: 24px; + font-size: 12px; + color: var(--normal-bg); + background: var(--normal-text); + margin-left: var(--toast-button-margin-start); + margin-right: var(--toast-button-margin-end); + border: none; + cursor: pointer; + outline: none; + display: flex; + align-items: center; + flex-shrink: 0; + transition: opacity 400ms, box-shadow 200ms; + } + + :where([data-sonner-toast]) :where([data-button]):focus-visible { + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4); + } + + :where([data-sonner-toast]) :where([data-button]):first-of-type { + margin-left: var(--toast-button-margin-start); + margin-right: var(--toast-button-margin-end); + } + + :where([data-sonner-toast]) :where([data-cancel]) { + color: var(--normal-text); + background: rgba(0, 0, 0, 0.08); + } + + :where([data-sonner-toast][data-theme='dark']) :where([data-cancel]) { + background: rgba(255, 255, 255, 0.3); + } + + :where([data-sonner-toast]) :where([data-close-button]) { + position: absolute; + left: var(--toast-close-button-start); + right: var(--toast-close-button-end); + top: 0; + height: 20px; + width: 20px; + display: flex; + justify-content: center; + align-items: center; + padding: 0; + background: var(--gray1); + color: var(--gray12); + border: 1px solid var(--gray4); + transform: var(--toast-close-button-transform); + border-radius: 50%; + cursor: pointer; + z-index: 1; + transition: opacity 100ms, background 200ms, border-color 200ms; + } + + :where([data-sonner-toast]) :where([data-close-button]):focus-visible { + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2); + } + + :where([data-sonner-toast]) :where([data-disabled='true']) { + cursor: not-allowed; + } + + :where([data-sonner-toast]):hover :where([data-close-button]):hover { + background: var(--gray2); + border-color: var(--gray5); + } + + /* Leave a ghost div to avoid setting hover to false when swiping out */ + :where([data-sonner-toast][data-swiping='true'])::before { + content: ''; + position: absolute; + left: 0; + right: 0; + height: 100%; + z-index: -1; + } + + :where([data-sonner-toast][data-y-position='top'][data-swiping='true'])::before { + /* y 50% needed to distribute height additional height evenly */ + bottom: 50%; + transform: scaleY(3) translateY(50%); + } + + :where([data-sonner-toast][data-y-position='bottom'][data-swiping='true'])::before { + /* y -50% needed to distribute height additional height evenly */ + top: 50%; + transform: scaleY(3) translateY(-50%); + } + + /* Leave a ghost div to avoid setting hover to false when transitioning out */ + :where([data-sonner-toast][data-swiping='false'][data-removed='true'])::before { + content: ''; + position: absolute; + inset: 0; + transform: scaleY(2); + } + + /* Needed to avoid setting hover to false when inbetween toasts */ + :where([data-sonner-toast])::after { + content: ''; + position: absolute; + left: 0; + height: calc(var(--gap) + 1px); + bottom: 100%; + width: 100%; + } + + :where([data-sonner-toast][data-mounted='true']) { + --y: translateY(0); + opacity: 1; + } + + :where([data-sonner-toast][data-expanded='false'][data-front='false']) { + --scale: var(--toasts-before) * 0.05 + 1; + --y: translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale))); + height: var(--front-toast-height); + } + + :where([data-sonner-toast]) > * { + transition: opacity 400ms; + } + + :where([data-sonner-toast][data-expanded='false'][data-front='false'][data-styled='true']) > * { + opacity: 0; + } + + :where([data-sonner-toast][data-visible='false']) { + opacity: 0; + pointer-events: none; + } + + :where([data-sonner-toast][data-mounted='true'][data-expanded='true']) { + --y: translateY(calc(var(--lift) * var(--offset))); + height: var(--initial-height); + } + + :where([data-sonner-toast][data-removed='true'][data-front='true'][data-swipe-out='false']) { + --y: translateY(calc(var(--lift) * -100%)); + opacity: 0; + } + + :where([data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='true']) { + --y: translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%)); + opacity: 0; + } + + :where([data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='false']) { + --y: translateY(40%); + opacity: 0; + transition: transform 500ms, opacity 200ms; + } + + /* Bump up the height to make sure hover state doesn't get set to false */ + :where([data-sonner-toast][data-removed='true'][data-front='false'])::before { + height: calc(var(--initial-height) + 20%); + } + + [data-sonner-toast][data-swiping='true'] { + transform: var(--y) translateY(var(--swipe-amount, 0px)); + transition: none; + } + + [data-sonner-toast][data-swipe-out='true'][data-y-position='bottom'], + [data-sonner-toast][data-swipe-out='true'][data-y-position='top'] { + animation: swipe-out 200ms ease-out forwards; + } + + @keyframes swipe-out { + from { + transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount))); + opacity: 1; + } + + to { + transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount) + var(--lift) * -100%)); + opacity: 0; + } + } + + @media (max-width: 600px) { + [data-sonner-toaster] { + position: fixed; + --mobile-offset: 16px; + right: var(--mobile-offset); + left: var(--mobile-offset); + width: 100%; + } + + [data-sonner-toaster] [data-sonner-toast] { + left: 0; + right: 0; + width: calc(100% - var(--mobile-offset) * 2); + } + + [data-sonner-toaster][data-x-position='left'] { + left: var(--mobile-offset); + } + + [data-sonner-toaster][data-y-position='bottom'] { + bottom: 20px; + } + + [data-sonner-toaster][data-y-position='top'] { + top: 20px; + } + + [data-sonner-toaster][data-x-position='center'] { + left: var(--mobile-offset); + right: var(--mobile-offset); + transform: none; + } + } + + [data-sonner-toaster][data-theme='light'] { + --normal-bg: #fff; + --normal-border: var(--gray4); + --normal-text: var(--gray12); + + --success-bg: hsl(143, 85%, 96%); + --success-border: hsl(145, 92%, 91%); + --success-text: hsl(140, 100%, 27%); + + --info-bg: hsl(208, 100%, 97%); + --info-border: hsl(221, 91%, 91%); + --info-text: hsl(210, 92%, 45%); + + --warning-bg: hsl(49, 100%, 97%); + --warning-border: hsl(49, 91%, 91%); + --warning-text: hsl(31, 92%, 45%); + + --error-bg: hsl(359, 100%, 97%); + --error-border: hsl(359, 100%, 94%); + --error-text: hsl(360, 100%, 45%); + } + + [data-sonner-toaster][data-theme='light'] [data-sonner-toast][data-invert='true'] { + --normal-bg: #000; + --normal-border: hsl(0, 0%, 20%); + --normal-text: var(--gray1); + } + + [data-sonner-toaster][data-theme='dark'] [data-sonner-toast][data-invert='true'] { + --normal-bg: #fff; + --normal-border: var(--gray3); + --normal-text: var(--gray12); + } + + [data-sonner-toaster][data-theme='dark'] { + --normal-bg: #000; + --normal-border: hsl(0, 0%, 20%); + --normal-text: var(--gray1); + + --success-bg: hsl(150, 100%, 6%); + --success-border: hsl(147, 100%, 12%); + --success-text: hsl(150, 86%, 65%); + + --info-bg: hsl(215, 100%, 6%); + --info-border: hsl(223, 100%, 12%); + --info-text: hsl(216, 87%, 65%); + + --warning-bg: hsl(64, 100%, 6%); + --warning-border: hsl(60, 100%, 12%); + --warning-text: hsl(46, 87%, 65%); + + --error-bg: hsl(358, 76%, 10%); + --error-border: hsl(357, 89%, 16%); + --error-text: hsl(358, 100%, 81%); + } + + [data-rich-colors='true'] [data-sonner-toast][data-type='success'] { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success-text); + } + + [data-rich-colors='true'] [data-sonner-toast][data-type='success'] [data-close-button] { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success-text); + } + + [data-rich-colors='true'] [data-sonner-toast][data-type='info'] { + background: var(--info-bg); + border-color: var(--info-border); + color: var(--info-text); + } + + [data-rich-colors='true'] [data-sonner-toast][data-type='info'] [data-close-button] { + background: var(--info-bg); + border-color: var(--info-border); + color: var(--info-text); + } + + [data-rich-colors='true'] [data-sonner-toast][data-type='warning'] { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning-text); + } + + [data-rich-colors='true'] [data-sonner-toast][data-type='warning'] [data-close-button] { + background: var(--warning-bg); + border-color: var(--warning-border); + color: var(--warning-text); + } + + [data-rich-colors='true'] [data-sonner-toast][data-type='error'] { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error-text); + } + + [data-rich-colors='true'] [data-sonner-toast][data-type='error'] [data-close-button] { + background: var(--error-bg); + border-color: var(--error-border); + color: var(--error-text); + } + + .sonner-loading-wrapper { + --size: 16px; + height: var(--size); + width: var(--size); + position: absolute; + inset: 0; + z-index: 10; + } + + .sonner-loading-wrapper[data-visible='false'] { + transform-origin: center; + animation: sonner-fade-out 0.2s ease forwards; + } + + .sonner-spinner { + position: relative; + top: 50%; + left: 50%; + height: var(--size); + width: var(--size); + } + + .sonner-loading-bar { + animation: sonner-spin 1.2s linear infinite; + background: var(--gray11); + border-radius: 6px; + height: 8%; + left: -10%; + position: absolute; + top: -3.9%; + width: 24%; + } + + .sonner-loading-bar:nth-child(1) { + animation-delay: -1.2s; + transform: rotate(0.0001deg) translate(146%); + } + + .sonner-loading-bar:nth-child(2) { + animation-delay: -1.1s; + transform: rotate(30deg) translate(146%); + } + + .sonner-loading-bar:nth-child(3) { + animation-delay: -1s; + transform: rotate(60deg) translate(146%); + } + + .sonner-loading-bar:nth-child(4) { + animation-delay: -0.9s; + transform: rotate(90deg) translate(146%); + } + + .sonner-loading-bar:nth-child(5) { + animation-delay: -0.8s; + transform: rotate(120deg) translate(146%); + } + + .sonner-loading-bar:nth-child(6) { + animation-delay: -0.7s; + transform: rotate(150deg) translate(146%); + } + + .sonner-loading-bar:nth-child(7) { + animation-delay: -0.6s; + transform: rotate(180deg) translate(146%); + } + + .sonner-loading-bar:nth-child(8) { + animation-delay: -0.5s; + transform: rotate(210deg) translate(146%); + } + + .sonner-loading-bar:nth-child(9) { + animation-delay: -0.4s; + transform: rotate(240deg) translate(146%); + } + + .sonner-loading-bar:nth-child(10) { + animation-delay: -0.3s; + transform: rotate(270deg) translate(146%); + } + + .sonner-loading-bar:nth-child(11) { + animation-delay: -0.2s; + transform: rotate(300deg) translate(146%); + } + + .sonner-loading-bar:nth-child(12) { + animation-delay: -0.1s; + transform: rotate(330deg) translate(146%); + } + + @keyframes sonner-fade-in { + 0% { + opacity: 0; + transform: scale(0.8); + } + 100% { + opacity: 1; + transform: scale(1); + } + } + + @keyframes sonner-fade-out { + 0% { + opacity: 1; + transform: scale(1); + } + 100% { + opacity: 0; + transform: scale(0.8); + } + } + + @keyframes sonner-spin { + 0% { + opacity: 1; + } + 100% { + opacity: 0.15; + } + } + + @media (prefers-reduced-motion) { + [data-sonner-toast], + [data-sonner-toast] > *, + .sonner-loading-bar { + transition: none !important; + animation: none !important; + } + } + + .sonner-loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + transform-origin: center; + transition: opacity 200ms, transform 200ms; + } + + .sonner-loader[data-visible='false'] { + opacity: 0; + transform: scale(0.8) translate(-50%, -50%); + } +`; + +export default styles; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index d20f644..34924dc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +// @deno-types="npm:@types/react@^18.2.0" import React from 'react'; export type ToastTypes = 'normal' | 'action' | 'success' | 'info' | 'warning' | 'error' | 'loading' | 'default';