diff --git a/apps/www/.vitepress/theme/config/docs.ts b/apps/www/.vitepress/theme/config/docs.ts index 727c003b..c96098f8 100644 --- a/apps/www/.vitepress/theme/config/docs.ts +++ b/apps/www/.vitepress/theme/config/docs.ts @@ -315,13 +315,11 @@ export const docsConfig: DocsConfig = { href: '/docs/components/textarea', items: [], }, - // { - // title: "Toast", - // href: "#", - // label: "Soon", - // disabled: true, - // items: [] - // }, + { + title: 'Toast', + href: '/docs/components/toast', + items: [], + }, { title: 'Toggle', href: '/docs/components/toggle', diff --git a/apps/www/.vitepress/theme/layout/MainLayout.vue b/apps/www/.vitepress/theme/layout/MainLayout.vue index 4c2b0a06..8b19bfb2 100644 --- a/apps/www/.vitepress/theme/layout/MainLayout.vue +++ b/apps/www/.vitepress/theme/layout/MainLayout.vue @@ -16,7 +16,8 @@ import RadixIconsMoon from '~icons/radix-icons/moon' import RadixIconsSun from '~icons/radix-icons/sun' import { useConfigStore } from '@/stores/config' import { Dialog, DialogContent } from '@/lib/registry/default/ui/dialog' -import { Toaster } from '@/lib/registry/default/ui/toast' +import { Toaster as DefaultToaster } from '@/lib/registry/default/ui/toast' +import { Toaster as NewYorkToaster } from '@/lib/registry/new-york/ui/toast' import File from '~icons/radix-icons/file' import Circle from '~icons/radix-icons/circle' @@ -276,6 +277,7 @@ watch(() => $route.path, (n) => { - + + diff --git a/apps/www/src/lib/registry/default/ui/toast/Toast.vue b/apps/www/src/lib/registry/default/ui/toast/Toast.vue index 72747db6..46e3bd2b 100644 --- a/apps/www/src/lib/registry/default/ui/toast/Toast.vue +++ b/apps/www/src/lib/registry/default/ui/toast/Toast.vue @@ -4,9 +4,11 @@ import { ToastRoot, type ToastRootEmits, type ToastRootProps, useEmitAsProps } f import { toastVariants } from '.' import { cn } from '@/lib/utils' +interface ToastVariantProps extends VariantProps {} + export interface ToastProps extends ToastRootProps { class?: string - variant?: NonNullable[0]>['variant'] + variant?: ToastVariantProps['variant'] }; const props = defineProps() diff --git a/apps/www/src/lib/registry/new-york/example/ToastDemo.vue b/apps/www/src/lib/registry/new-york/example/ToastDemo.vue new file mode 100644 index 00000000..041565e1 --- /dev/null +++ b/apps/www/src/lib/registry/new-york/example/ToastDemo.vue @@ -0,0 +1,19 @@ + + + + { + toast({ + title: 'Scheduled: Catch up', + description: 'Friday, February 10, 2023 at 5:57 PM', + }); + }" + > + Add to calander + + diff --git a/apps/www/src/lib/registry/new-york/example/ToastDestructive.vue b/apps/www/src/lib/registry/new-york/example/ToastDestructive.vue new file mode 100644 index 00000000..b58f885c --- /dev/null +++ b/apps/www/src/lib/registry/new-york/example/ToastDestructive.vue @@ -0,0 +1,27 @@ + + + + { + toast({ + title: 'Uh oh! Something went wrong.', + description: 'There was a problem with your request.', + variant: 'destructive', + action: h(ToastAction, { + altText: 'Try again', + }, { + default: () => 'Try again', + }), + }); + }" + > + Show Toast + + diff --git a/apps/www/src/lib/registry/new-york/example/ToastSimple.vue b/apps/www/src/lib/registry/new-york/example/ToastSimple.vue new file mode 100644 index 00000000..620038fd --- /dev/null +++ b/apps/www/src/lib/registry/new-york/example/ToastSimple.vue @@ -0,0 +1,18 @@ + + + + { + toast({ + description: 'Your message has been sent.', + }); + }" + > + Show Toast + + diff --git a/apps/www/src/lib/registry/new-york/example/ToastWithAction.vue b/apps/www/src/lib/registry/new-york/example/ToastWithAction.vue new file mode 100644 index 00000000..a56e3fba --- /dev/null +++ b/apps/www/src/lib/registry/new-york/example/ToastWithAction.vue @@ -0,0 +1,26 @@ + + + + { + toast({ + title: 'Uh oh! Something went wrong.', + description: 'There was a problem with your request.', + action: h(ToastAction, { + altText: 'Try again', + }, { + default: () => 'Try again', + }), + }); + }" + > + Show Toast + + diff --git a/apps/www/src/lib/registry/new-york/example/ToastWithTitle.vue b/apps/www/src/lib/registry/new-york/example/ToastWithTitle.vue new file mode 100644 index 00000000..5f30a0d2 --- /dev/null +++ b/apps/www/src/lib/registry/new-york/example/ToastWithTitle.vue @@ -0,0 +1,19 @@ + + + + { + toast({ + title: 'Uh oh! Something went wrong.', + description: 'There was a problem with your request.', + }); + }" + > + Show Toast + + diff --git a/apps/www/src/lib/registry/new-york/ui/toast/Toast.vue b/apps/www/src/lib/registry/new-york/ui/toast/Toast.vue new file mode 100644 index 00000000..46e3bd2b --- /dev/null +++ b/apps/www/src/lib/registry/new-york/ui/toast/Toast.vue @@ -0,0 +1,26 @@ + + + + + + + diff --git a/apps/www/src/lib/registry/new-york/ui/toast/ToastAction.vue b/apps/www/src/lib/registry/new-york/ui/toast/ToastAction.vue new file mode 100644 index 00000000..20747e70 --- /dev/null +++ b/apps/www/src/lib/registry/new-york/ui/toast/ToastAction.vue @@ -0,0 +1,12 @@ + + + + + + + diff --git a/apps/www/src/lib/registry/new-york/ui/toast/ToastClose.vue b/apps/www/src/lib/registry/new-york/ui/toast/ToastClose.vue new file mode 100644 index 00000000..183e1aeb --- /dev/null +++ b/apps/www/src/lib/registry/new-york/ui/toast/ToastClose.vue @@ -0,0 +1,15 @@ + + + + + + + diff --git a/apps/www/src/lib/registry/new-york/ui/toast/ToastDescription.vue b/apps/www/src/lib/registry/new-york/ui/toast/ToastDescription.vue new file mode 100644 index 00000000..98630e3d --- /dev/null +++ b/apps/www/src/lib/registry/new-york/ui/toast/ToastDescription.vue @@ -0,0 +1,12 @@ + + + + + + + diff --git a/apps/www/src/lib/registry/new-york/ui/toast/ToastProvider.vue b/apps/www/src/lib/registry/new-york/ui/toast/ToastProvider.vue new file mode 100644 index 00000000..340cbd83 --- /dev/null +++ b/apps/www/src/lib/registry/new-york/ui/toast/ToastProvider.vue @@ -0,0 +1,11 @@ + + + + + + + diff --git a/apps/www/src/lib/registry/new-york/ui/toast/ToastTitle.vue b/apps/www/src/lib/registry/new-york/ui/toast/ToastTitle.vue new file mode 100644 index 00000000..0230bd31 --- /dev/null +++ b/apps/www/src/lib/registry/new-york/ui/toast/ToastTitle.vue @@ -0,0 +1,12 @@ + + + + + + + diff --git a/apps/www/src/lib/registry/new-york/ui/toast/ToastViewport.vue b/apps/www/src/lib/registry/new-york/ui/toast/ToastViewport.vue new file mode 100644 index 00000000..5f2f2b95 --- /dev/null +++ b/apps/www/src/lib/registry/new-york/ui/toast/ToastViewport.vue @@ -0,0 +1,10 @@ + + + + + diff --git a/apps/www/src/lib/registry/new-york/ui/toast/Toaster.vue b/apps/www/src/lib/registry/new-york/ui/toast/Toaster.vue new file mode 100644 index 00000000..d5db5dcd --- /dev/null +++ b/apps/www/src/lib/registry/new-york/ui/toast/Toaster.vue @@ -0,0 +1,24 @@ + + + + + + + + {{ toast.title }} + + + {{ toast.description }} + + + + + + + + diff --git a/apps/www/src/lib/registry/new-york/ui/toast/index.ts b/apps/www/src/lib/registry/new-york/ui/toast/index.ts new file mode 100644 index 00000000..17f038c7 --- /dev/null +++ b/apps/www/src/lib/registry/new-york/ui/toast/index.ts @@ -0,0 +1,26 @@ +export { default as Toaster } from './Toaster.vue' +export { default as Toast } from './Toast.vue' +export { default as ToastViewport } from './ToastViewport.vue' +export { default as ToastAction } from './ToastAction.vue' +export { default as ToastClose } from './ToastClose.vue' +export { default as ToastTitle } from './ToastTitle.vue' +export { default as ToastDescription } from './ToastDescription.vue' +export { default as ToastProvider } from './ToastProvider.vue' + +import { cva } from 'class-variance-authority' + +export const toastVariants = cva( + 'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full', + { + variants: { + variant: { + default: 'border bg-background text-foreground', + destructive: + 'destructive group border-destructive bg-destructive text-destructive-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) diff --git a/apps/www/src/lib/registry/new-york/ui/toast/use-toast.ts b/apps/www/src/lib/registry/new-york/ui/toast/use-toast.ts new file mode 100644 index 00000000..f21f6ebb --- /dev/null +++ b/apps/www/src/lib/registry/new-york/ui/toast/use-toast.ts @@ -0,0 +1,160 @@ +import { computed, ref } from 'vue' +import type { Component } from 'vue' +import type { ToastProps } from './Toast.vue' + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: string + description?: string + action?: Component +} + +const actionTypes = { + ADD_TOAST: 'ADD_TOAST', + UPDATE_TOAST: 'UPDATE_TOAST', + DISMISS_TOAST: 'DISMISS_TOAST', + REMOVE_TOAST: 'REMOVE_TOAST', +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_VALUE + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType['ADD_TOAST'] + toast: ToasterToast + } + | { + type: ActionType['UPDATE_TOAST'] + toast: Partial + } + | { + type: ActionType['DISMISS_TOAST'] + toastId?: ToasterToast['id'] + } + | { + type: ActionType['REMOVE_TOAST'] + toastId?: ToasterToast['id'] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +function addToRemoveQueue(toastId: string) { + if (toastTimeouts.has(toastId)) + return + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: actionTypes.REMOVE_TOAST, + toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +const state = ref({ + toasts: [], +}) + +function dispatch(action: Action) { + switch (action.type) { + case actionTypes.ADD_TOAST: + state.value.toasts = [action.toast, ...state.value.toasts].slice(0, TOAST_LIMIT) + break + + case actionTypes.UPDATE_TOAST: + state.value.toasts = state.value.toasts.map(t => + t.id === action.toast.id ? { ...t, ...action.toast } : t, + ) + break + + case actionTypes.DISMISS_TOAST: { + const { toastId } = action + + if (toastId) { + addToRemoveQueue(toastId) + } + else { + state.value.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + state.value.toasts = state.value.toasts.map(t => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t, + ) + break + } + + case actionTypes.REMOVE_TOAST: + if (action.toastId === undefined) + state.value.toasts = [] + else + state.value.toasts = state.value.toasts.filter(t => t.id !== action.toastId) + + break + } +} + +function useToast() { + return { + toasts: computed(() => state.value.toasts), + toast, + dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }), + } +} + +type Toast = Omit + +function toast(props: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: actionTypes.UPDATE_TOAST, + toast: { ...props, id }, + }) + + const dismiss = () => dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id }) + + dispatch({ + type: actionTypes.ADD_TOAST, + toast: { + ...props, + id, + 'open': true, + 'onUpdate:open': (open: boolean) => { + if (!open) + dismiss() + }, + }, + }) + + return { + id, + dismiss, + update, + } +} + +export { toast, useToast }