Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Toaster component for displaying toast notifications after … #1

Merged
merged 1 commit into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions app.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
<template>
<NuxtLoadingIndicator :color="false" class="z-100 bg-primary" />
<NuxtPage />
<main>
<NuxtLoadingIndicator :color="false" class="z-100 bg-primary" />
<NuxtPage />
<Toaster />
</main>
</template>

<script setup lang="ts">
import Toaster from '@/components/ui/toast/Toaster.vue';

const config = useConfig();

useSeoMeta({
Expand Down
20 changes: 15 additions & 5 deletions components/content/CodeCopy.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
v-if="copied === false"
name="lucide:copy"
class="self-center cursor-pointer text-muted-foreground hover:text-primary"
@click="copyCode"
@click="handleClick"
/>
<Icon
v-else
Expand All @@ -18,17 +18,27 @@
</template>

<script setup lang="ts">
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/toast/use-toast';
import { Toaster } from '@/components/ui/toast';

const props = defineProps<{
code: string;
}>();

const { toast } = useToast();

const { copy } = useClipboard({ source: props.code });
const copied = ref(false);
function copyCode() {
copy(props.code).then(
() => { copied.value = true; },
);

async function handleClick() {
await copy(props.code);
copied.value = true;
toast({
description: 'Copied to clipboard!',
});
}

const checkIconRef = ref<HTMLElement>();
onClickOutside(checkIconRef, () => {
copied.value = false;
Expand Down
28 changes: 28 additions & 0 deletions components/ui/toast/Toast.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script setup lang="ts">
import { computed } from 'vue'
import { ToastRoot, type ToastRootEmits, useForwardPropsEmits } from 'radix-vue'
import { type ToastProps, toastVariants } from '.'
import { cn } from '@/lib/utils'

const props = defineProps<ToastProps>()

const emits = defineEmits<ToastRootEmits>()

const delegatedProps = computed(() => {
const { class: _, ...delegated } = props

return delegated
})

const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

<template>
<ToastRoot
v-bind="forwarded"
:class="cn(toastVariants({ variant }), props.class)"
@update:open="onOpenChange"
>
<slot />
</ToastRoot>
</template>
19 changes: 19 additions & 0 deletions components/ui/toast/ToastAction.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { ToastAction, type ToastActionProps } from 'radix-vue'
import { cn } from '@/lib/utils'

const props = defineProps<ToastActionProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = computed(() => {
const { class: _, ...delegated } = props

return delegated
})
</script>

<template>
<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 ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 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)">
<slot />
</ToastAction>
</template>
22 changes: 22 additions & 0 deletions components/ui/toast/ToastClose.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { ToastClose, type ToastCloseProps } from 'radix-vue'
import { X } from 'lucide-vue-next'
import { cn } from '@/lib/utils'

const props = defineProps<ToastCloseProps & {
class?: HTMLAttributes['class']
}>()

const delegatedProps = computed(() => {
const { class: _, ...delegated } = props

return delegated
})
</script>

<template>
<ToastClose v-bind="delegatedProps" :class="cn('absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 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)">
<X class="h-4 w-4" />
</ToastClose>
</template>
19 changes: 19 additions & 0 deletions components/ui/toast/ToastDescription.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { ToastDescription, type ToastDescriptionProps } from 'radix-vue'
import { cn } from '@/lib/utils'

const props = defineProps<ToastDescriptionProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = computed(() => {
const { class: _, ...delegated } = props

return delegated
})
</script>

<template>
<ToastDescription :class="cn('text-sm opacity-90', props.class)" v-bind="delegatedProps">
<slot />
</ToastDescription>
</template>
11 changes: 11 additions & 0 deletions components/ui/toast/ToastProvider.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script setup lang="ts">
import { ToastProvider, type ToastProviderProps } from 'radix-vue'

const props = defineProps<ToastProviderProps>()
</script>

<template>
<ToastProvider v-bind="props">
<slot />
</ToastProvider>
</template>
19 changes: 19 additions & 0 deletions components/ui/toast/ToastTitle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { ToastTitle, type ToastTitleProps } from 'radix-vue'
import { cn } from '@/lib/utils'

const props = defineProps<ToastTitleProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = computed(() => {
const { class: _, ...delegated } = props

return delegated
})
</script>

<template>
<ToastTitle v-bind="delegatedProps" :class="cn('text-sm font-semibold', props.class)">
<slot />
</ToastTitle>
</template>
17 changes: 17 additions & 0 deletions components/ui/toast/ToastViewport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { ToastViewport, type ToastViewportProps } from 'radix-vue'
import { cn } from '@/lib/utils'

const props = defineProps<ToastViewportProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = computed(() => {
const { class: _, ...delegated } = props

return delegated
})
</script>

<template>
<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)" />
</template>
30 changes: 30 additions & 0 deletions components/ui/toast/Toaster.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script setup lang="ts">
import { isVNode } from 'vue'
import { useToast } from './use-toast'
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from '.'

const { toasts } = useToast()
</script>

<template>
<ToastProvider>
<Toast v-for="toast in toasts" :key="toast.id" v-bind="toast">
<div class="grid gap-1">
<ToastTitle v-if="toast.title">
{{ toast.title }}
</ToastTitle>
<template v-if="toast.description">
<ToastDescription v-if="isVNode(toast.description)">
<component :is="toast.description" />
</ToastDescription>
<ToastDescription v-else>
{{ toast.description }}
</ToastDescription>
</template>
<ToastClose />
</div>
<component :is="toast.action" />
</Toast>
<ToastViewport />
</ToastProvider>
</template>
38 changes: 38 additions & 0 deletions components/ui/toast/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { ToastRootProps } from 'radix-vue'
import type { HTMLAttributes } from 'vue'

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'
export { toast, useToast } from './use-toast'

import { type VariantProps, 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-[--radix-toast-swipe-end-x] data-[swipe=move]:translate-x-[--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',
},
},
)

type ToastVariants = VariantProps<typeof toastVariants>

export interface ToastProps extends ToastRootProps {
class?: HTMLAttributes['class']
variant?: ToastVariants['variant']
onOpenChange?: ((value: boolean) => void) | undefined
}
Loading
Loading