73 lines
12 KiB
JSON
73 lines
12 KiB
JSON
{
|
|
"name": "toast",
|
|
"type": "registry:ui",
|
|
"dependencies": [
|
|
"reka-ui"
|
|
],
|
|
"registryDependencies": [
|
|
"utils"
|
|
],
|
|
"files": [
|
|
{
|
|
"path": "ui/toast/Toast.vue",
|
|
"content": "<script setup lang=\"ts\">\nimport { cn } from '@/lib/utils'\nimport { ToastRoot, type ToastRootEmits, useForwardPropsEmits } from 'reka-ui'\nimport { computed } from 'vue'\nimport { type ToastProps, toastVariants } from '.'\n\nconst props = defineProps<ToastProps>()\n\nconst emits = defineEmits<ToastRootEmits>()\n\nconst delegatedProps = computed(() => {\n const { class: _, ...delegated } = props\n\n return delegated\n})\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits)\n</script>\n\n<template>\n <ToastRoot\n v-bind=\"forwarded\"\n :class=\"cn(toastVariants({ variant }), props.class)\"\n @update:open=\"onOpenChange\"\n >\n <slot />\n </ToastRoot>\n</template>\n",
|
|
"type": "registry:ui",
|
|
"target": ""
|
|
},
|
|
{
|
|
"path": "ui/toast/ToastAction.vue",
|
|
"content": "<script setup lang=\"ts\">\nimport { cn } from '@/lib/utils'\nimport { ToastAction, type ToastActionProps } from 'reka-ui'\nimport { computed, type HTMLAttributes } from 'vue'\n\nconst props = defineProps<ToastActionProps & { class?: HTMLAttributes['class'] }>()\n\nconst delegatedProps = computed(() => {\n const { class: _, ...delegated } = props\n\n return delegated\n})\n</script>\n\n<template>\n <ToastAction v-bind=\"delegatedProps\" :class=\"cn('inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive', props.class)\">\n <slot />\n </ToastAction>\n</template>\n",
|
|
"type": "registry:ui",
|
|
"target": ""
|
|
},
|
|
{
|
|
"path": "ui/toast/ToastClose.vue",
|
|
"content": "<script setup lang=\"ts\">\nimport { cn } from '@/lib/utils'\nimport { Cross2Icon } from '@radix-icons/vue'\nimport { ToastClose, type ToastCloseProps } from 'reka-ui'\nimport { computed, type HTMLAttributes } from 'vue'\n\nconst props = defineProps<ToastCloseProps & {\n class?: HTMLAttributes['class']\n}>()\n\nconst delegatedProps = computed(() => {\n const { class: _, ...delegated } = props\n\n return delegated\n})\n</script>\n\n<template>\n <ToastClose v-bind=\"delegatedProps\" :class=\"cn('absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600', props.class)\">\n <Cross2Icon class=\"h-4 w-4\" />\n </ToastClose>\n</template>\n",
|
|
"type": "registry:ui",
|
|
"target": ""
|
|
},
|
|
{
|
|
"path": "ui/toast/ToastDescription.vue",
|
|
"content": "<script setup lang=\"ts\">\nimport { cn } from '@/lib/utils'\nimport { ToastDescription, type ToastDescriptionProps } from 'reka-ui'\nimport { computed, type HTMLAttributes } from 'vue'\n\nconst props = defineProps<ToastDescriptionProps & { class?: HTMLAttributes['class'] }>()\n\nconst delegatedProps = computed(() => {\n const { class: _, ...delegated } = props\n\n return delegated\n})\n</script>\n\n<template>\n <ToastDescription :class=\"cn('text-sm opacity-90', props.class)\" v-bind=\"delegatedProps\">\n <slot />\n </ToastDescription>\n</template>\n",
|
|
"type": "registry:ui",
|
|
"target": ""
|
|
},
|
|
{
|
|
"path": "ui/toast/ToastProvider.vue",
|
|
"content": "<script setup lang=\"ts\">\nimport { ToastProvider, type ToastProviderProps } from 'reka-ui'\n\nconst props = defineProps<ToastProviderProps>()\n</script>\n\n<template>\n <ToastProvider v-bind=\"props\">\n <slot />\n </ToastProvider>\n</template>\n",
|
|
"type": "registry:ui",
|
|
"target": ""
|
|
},
|
|
{
|
|
"path": "ui/toast/ToastTitle.vue",
|
|
"content": "<script setup lang=\"ts\">\nimport { cn } from '@/lib/utils'\nimport { ToastTitle, type ToastTitleProps } from 'reka-ui'\nimport { computed, type HTMLAttributes } from 'vue'\n\nconst props = defineProps<ToastTitleProps & { class?: HTMLAttributes['class'] }>()\n\nconst delegatedProps = computed(() => {\n const { class: _, ...delegated } = props\n\n return delegated\n})\n</script>\n\n<template>\n <ToastTitle v-bind=\"delegatedProps\" :class=\"cn('text-sm font-semibold [&+div]:text-xs', props.class)\">\n <slot />\n </ToastTitle>\n</template>\n",
|
|
"type": "registry:ui",
|
|
"target": ""
|
|
},
|
|
{
|
|
"path": "ui/toast/ToastViewport.vue",
|
|
"content": "<script setup lang=\"ts\">\nimport { cn } from '@/lib/utils'\nimport { ToastViewport, type ToastViewportProps } from 'reka-ui'\nimport { computed, type HTMLAttributes } from 'vue'\n\nconst props = defineProps<ToastViewportProps & { class?: HTMLAttributes['class'] }>()\n\nconst delegatedProps = computed(() => {\n const { class: _, ...delegated } = props\n\n return delegated\n})\n</script>\n\n<template>\n <ToastViewport v-bind=\"delegatedProps\" :class=\"cn('fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]', props.class)\" />\n</template>\n",
|
|
"type": "registry:ui",
|
|
"target": ""
|
|
},
|
|
{
|
|
"path": "ui/toast/Toaster.vue",
|
|
"content": "<script setup lang=\"ts\">\nimport { isVNode } from 'vue'\nimport { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from '.'\nimport { useToast } from './use-toast'\n\nconst { toasts } = useToast()\n</script>\n\n<template>\n <ToastProvider>\n <Toast v-for=\"toast in toasts\" :key=\"toast.id\" v-bind=\"toast\">\n <div class=\"grid gap-1\">\n <ToastTitle v-if=\"toast.title\">\n {{ toast.title }}\n </ToastTitle>\n <template v-if=\"toast.description\">\n <ToastDescription v-if=\"isVNode(toast.description)\">\n <component :is=\"toast.description\" />\n </ToastDescription>\n <ToastDescription v-else>\n {{ toast.description }}\n </ToastDescription>\n </template>\n <ToastClose />\n </div>\n <component :is=\"toast.action\" />\n </Toast>\n <ToastViewport />\n </ToastProvider>\n</template>\n",
|
|
"type": "registry:ui",
|
|
"target": ""
|
|
},
|
|
{
|
|
"path": "ui/toast/index.ts",
|
|
"content": "import type { ToastRootProps } from 'reka-ui'\nimport type { HTMLAttributes } from 'vue'\n\nexport { default as Toast } from './Toast.vue'\nexport { default as ToastAction } from './ToastAction.vue'\nexport { default as ToastClose } from './ToastClose.vue'\nexport { default as ToastDescription } from './ToastDescription.vue'\nexport { default as Toaster } from './Toaster.vue'\nexport { default as ToastProvider } from './ToastProvider.vue'\nexport { default as ToastTitle } from './ToastTitle.vue'\nexport { default as ToastViewport } from './ToastViewport.vue'\nexport { toast, useToast } from './use-toast'\n\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nexport const toastVariants = cva(\n 'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--reka-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--reka-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',\n {\n variants: {\n variant: {\n default: 'border bg-background text-foreground',\n destructive:\n 'destructive group border-destructive bg-destructive text-destructive-foreground',\n },\n },\n defaultVariants: {\n variant: 'default',\n },\n },\n)\n\ntype ToastVariants = VariantProps<typeof toastVariants>\n\nexport interface ToastProps extends ToastRootProps {\n class?: HTMLAttributes['class']\n variant?: ToastVariants['variant']\n onOpenChange?: ((value: boolean) => void) | undefined\n}\n",
|
|
"type": "registry:ui",
|
|
"target": ""
|
|
},
|
|
{
|
|
"path": "ui/toast/use-toast.ts",
|
|
"content": "import type { Component, VNode } from 'vue'\nimport type { ToastProps } from '.'\nimport { computed, ref } from 'vue'\n\nconst TOAST_LIMIT = 1\nconst TOAST_REMOVE_DELAY = 1000000\n\nexport type StringOrVNode =\n | string\n | VNode\n | (() => VNode)\n\ntype ToasterToast = ToastProps & {\n id: string\n title?: string\n description?: StringOrVNode\n action?: Component\n}\n\nconst actionTypes = {\n ADD_TOAST: 'ADD_TOAST',\n UPDATE_TOAST: 'UPDATE_TOAST',\n DISMISS_TOAST: 'DISMISS_TOAST',\n REMOVE_TOAST: 'REMOVE_TOAST',\n} as const\n\nlet count = 0\n\nfunction genId() {\n count = (count + 1) % Number.MAX_VALUE\n return count.toString()\n}\n\ntype ActionType = typeof actionTypes\n\ntype Action =\n | {\n type: ActionType['ADD_TOAST']\n toast: ToasterToast\n }\n | {\n type: ActionType['UPDATE_TOAST']\n toast: Partial<ToasterToast>\n }\n | {\n type: ActionType['DISMISS_TOAST']\n toastId?: ToasterToast['id']\n }\n | {\n type: ActionType['REMOVE_TOAST']\n toastId?: ToasterToast['id']\n }\n\ninterface State {\n toasts: ToasterToast[]\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()\n\nfunction addToRemoveQueue(toastId: string) {\n if (toastTimeouts.has(toastId))\n return\n\n const timeout = setTimeout(() => {\n toastTimeouts.delete(toastId)\n dispatch({\n type: actionTypes.REMOVE_TOAST,\n toastId,\n })\n }, TOAST_REMOVE_DELAY)\n\n toastTimeouts.set(toastId, timeout)\n}\n\nconst state = ref<State>({\n toasts: [],\n})\n\nfunction dispatch(action: Action) {\n switch (action.type) {\n case actionTypes.ADD_TOAST:\n state.value.toasts = [action.toast, ...state.value.toasts].slice(0, TOAST_LIMIT)\n break\n\n case actionTypes.UPDATE_TOAST:\n state.value.toasts = state.value.toasts.map(t =>\n t.id === action.toast.id ? { ...t, ...action.toast } : t,\n )\n break\n\n case actionTypes.DISMISS_TOAST: {\n const { toastId } = action\n\n if (toastId) {\n addToRemoveQueue(toastId)\n }\n else {\n state.value.toasts.forEach((toast) => {\n addToRemoveQueue(toast.id)\n })\n }\n\n state.value.toasts = state.value.toasts.map(t =>\n t.id === toastId || toastId === undefined\n ? {\n ...t,\n open: false,\n }\n : t,\n )\n break\n }\n\n case actionTypes.REMOVE_TOAST:\n if (action.toastId === undefined)\n state.value.toasts = []\n else\n state.value.toasts = state.value.toasts.filter(t => t.id !== action.toastId)\n\n break\n }\n}\n\nfunction useToast() {\n return {\n toasts: computed(() => state.value.toasts),\n toast,\n dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }),\n }\n}\n\ntype Toast = Omit<ToasterToast, 'id'>\n\nfunction toast(props: Toast) {\n const id = genId()\n\n const update = (props: ToasterToast) =>\n dispatch({\n type: actionTypes.UPDATE_TOAST,\n toast: { ...props, id },\n })\n\n const dismiss = () => dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id })\n\n dispatch({\n type: actionTypes.ADD_TOAST,\n toast: {\n ...props,\n id,\n open: true,\n onOpenChange: (open: boolean) => {\n if (!open)\n dismiss()\n },\n },\n })\n\n return {\n id,\n dismiss,\n update,\n }\n}\n\nexport { toast, useToast }\n",
|
|
"type": "registry:ui",
|
|
"target": ""
|
|
}
|
|
]
|
|
}
|