Move all base components to an isolated folder

This commit is contained in:
2024-11-04 13:34:23 +01:00
parent 8a19448a38
commit 5ce2d3e236
27 changed files with 28 additions and 11 deletions

View File

@@ -0,0 +1,22 @@
<template>
<AvatarRoot class="inline-flex h-12 w-12 select-none items-center justify-center overflow-hidden align-middle">
<AvatarImage class="h-full w-full object-cover" :src="src" asChild @loading-status-change="(status) => loading = status === 'loading'">
<img :src="src" />
</AvatarImage>
<AvatarFallback :delay-ms="0" class="text-light-100 dark:text-dark-100 leading-1 flex h-full w-full items-center justify-center bg-light-25 dark:bg-dark-25 font-medium">
<Loading v-if="loading" />
<Icon v-else-if="!!icon" :icon="icon" class="w-8 h-8" />
<span v-else-if="!!text">{{ text }}</span>
</AvatarFallback>
</AvatarRoot>
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
const { src, icon, text } = defineProps<{
src: string
icon?: string
text?: string
}>();
const loading = ref(true);
</script>

View File

@@ -0,0 +1,13 @@
<template>
<button class="text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40"
:class="{'p-1': icon, 'h-[35px] px-[15px]': !icon}" >
<slot />
</button>
</template>
<script setup lang="ts">
const { icon = false } = defineProps<{
icon?: boolean
}>();
</script>

View File

@@ -0,0 +1,46 @@
<template>
<CollapsibleRoot v-model:open="model" :disabled="disabled">
<div class="flex flex-row justify-center items-center">
<span v-if="!!label">{{ label }}</span>
<CollapsibleTrigger class="ms-4" asChild>
<Button icon :disabled="disabled">
<Icon v-if="model" icon="radix-icons:cross-2" class="h-4 w-4" />
<Icon v-else icon="radix-icons:row-spacing" class="h-4 w-4" />
</Button>
</CollapsibleTrigger>
</div>
<slot name="alwaysVisible"></slot>
<CollapsibleContent class="overflow-hidden data-[state=closed]:animate-[collapseClose_0.2s_ease-in-out] data-[state=open]:animate-[collapseOpen_0.2s_ease-in-out]">
<slot></slot>
</CollapsibleContent>
</CollapsibleRoot>
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
const { label, disabled = false } = defineProps<{
label?: string
disabled?: boolean
}>();
const model = defineModel<boolean>();
</script>
<style>
@keyframes collapseOpen {
from {
height: 0;
}
to {
height: var(--radix-collapsible-content-height);
}
}
@keyframes collapseClose {
from {
height: var(--radix-collapsible-content-height);
}
to {
height: 0;
}
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<DialogRoot v-if="!priority">
<DialogTrigger asChild><Button>{{ label }}</Button></DialogTrigger>
<DialogPortal v-if="!disabled">
<DialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
<DialogContent class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
<DialogTitle v-if="!!title" class="text-3xl font-light relative -top-2">{{ title }}</DialogTitle>
<DialogDescription v-if="!!description" class="text-base pb-2">{{ description }}</DialogDescription>
<slot />
<DialogClose v-if="iconClose" class="text-light-100 dark:text-dark-100 absolute top-2 right-2 inline-flex h-6 w-6 appearance-none items-center justify-center outline-none text-xl" aria-label="Close">
<span aria-hidden>×</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</DialogRoot>
<AlertDialogRoot v-else>
<AlertDialogTrigger asChild><Button>{{ label }}</Button></AlertDialogTrigger>
<AlertDialogPortal v-if="!disabled">
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
<AlertDialogContent class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
<AlertDialogTitle v-if="!!title" class="text-3xl font-light relative -top-2">{{ title }}</AlertDialogTitle>
<AlertDialogDescription v-if="!!description" class="text-base pb-2">{{ description }}</AlertDialogDescription>
<slot />
</AlertDialogContent>
</AlertDialogPortal>
</AlertDialogRoot>
</template>
<script setup lang="ts">
const { label, title, description, priority = false, disabled = false, iconClose = true } = defineProps<{
label?: string
title?: string
description?: string
priority?: boolean
disabled?: boolean
iconClose?: boolean
}>();
</script>

View File

@@ -0,0 +1,21 @@
<template>
<HoverCardRoot :open-delay="delay">
<HoverCardTrigger class="inline-block cursor-help outline-none" asChild>
<slot></slot>
</HoverCardTrigger>
<HoverCardPortal v-if="!disabled">
<HoverCardContent :side="side" class="data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 p-5 data-[state=open]:transition-all text-light-100 dark:text-dark-100" >
<slot name="content"></slot>
<HoverCardArrow class="fill-light-35 dark:fill-dark-35" />
</HoverCardContent>
</HoverCardPortal>
</HoverCardRoot>
</template>
<script setup lang="ts">
const { delay = 500, disabled = false, side = 'bottom' } = defineProps<{
delay?: number
disabled?: boolean
side?: 'top' | 'right' | 'bottom' | 'left'
}>();
</script>

View File

@@ -0,0 +1,9 @@
<template>
<span :class="{'w-6 h-6 border-4 after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 border-2 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 border-[6px] after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}" class="rounded-full border-light-35 dark:border-dark-35 after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin"></span>
</template>
<script setup lang="ts">
const { size = 'normal' } = defineProps<{
size?: 'small' | 'normal' | 'large'
}>();
</script>

View File

@@ -0,0 +1,23 @@
<template>
<Label class="my-2 flex">{{ label }}
<NumberFieldRoot :min="min" :max="max" v-model="model" :disabled="disabled" :step="step" class="ms-4 flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
<NumberFieldDecrement class="data-[disabled]:opacity-50 px-1"><Icon icon="radix-icons:minus" :inline="true" class="w-6 text-light-100 dark:text-dark-100 opacity-100" /></NumberFieldDecrement>
<NumberFieldInput class="text-sm tabular-nums w-20 appearance-none bg-transparent px-2 py-1 outline-none caret-light-50 dark:caret-dark-50" />
<NumberFieldIncrement class="data-[disabled]:opacity-50 px-1"><Icon icon="radix-icons:plus" :inline="true" class="w-6 text-light-100 dark:text-dark-100 opacity-100" /></NumberFieldIncrement>
</NumberFieldRoot>
</Label>
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
const { min = 0, max = 100, disabled = false, step = 1, label } = defineProps<{
min?: number
max?: number
disabled?: boolean
step?: number
label?: string
}>();
const model = defineModel<number>();
</script>

View File

@@ -0,0 +1,20 @@
<template>
<Label class="my-2">{{ label }}
<PinInputRoot :disabled="disabled" :default-value="model?.split('')" @update:model-value="(v) => model = v.join('')" @complete="() => emit('complete')" class="flex gap-2 items-center mt-1">
<PinInputInput :type="hidden ? 'password' : undefined" v-for="(id, index) in amount" :key="id" :index="index" class="border border-light-35 dark:border-dark-35 w-10 h-10 outline-none
bg-light-20 dark:bg-dark-20 text-center text-light-100 dark:text-dark-100 placeholder:text-light-60 dark:placeholder:text-dark-60 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 caret-light-50 dark:caret-dark-50" />
</PinInputRoot>
</Label>
</template>
<script setup lang="ts">
const { hidden = false, amount, label, disabled = false } = defineProps<{
hidden?: boolean
label?: string
amount: number
disabled?: boolean
}>();
const model = defineModel<string>();
const emit = defineEmits(['complete']);
</script>

View File

@@ -0,0 +1,13 @@
<template>
<ProgressRoot :max="max" v-model="model" class="my-2 relative overflow-hidden bg-light-25 dark:bg-dark-25 w-48 h-3 data-[shape=thin]:h-1 data-[shape=large]:h-6" :data-shape="shape" style="transform: translateZ(0)" >
<ProgressIndicator class="bg-light-50 dark:bg-dark-50 h-full transition-[width] duration-[660ms] ease-[cubic-bezier(0.65, 0, 0.35, 1)]" :style="`width: ${((model ?? 0) / max) * 100}%`" />
</ProgressRoot>
</template>
<script setup lang="ts">
const { max = 100, shape = 'normal' } = defineProps<{
max?: number
shape?: 'thin' | 'normal' | 'large'
}>();
const model = defineModel<number>();
</script>

View File

@@ -0,0 +1,30 @@
<template>
<RadioGroupRoot :disabled="disabled" v-model="model" class="flex flex-col gap-2 p-2">
<Label v-for="option in options" class="flex items-center gap-2">
<RadioGroupItem :disabled="(option as RadioOption).disabled ?? false"
:value="(option as RadioOption).value ?? option"
class="border border-light-60 dark:border-dark-35 bg-light-20 dark:bg-dark-25 w-5 h-5 outline-none cursor-default hover:border-light-70 dark:hover:border-dark-40
focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 data-[disabled]:bg-light-10 dark:data-[disabled]:bg-dark-10 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20">
<RadioGroupIndicator>
<Icon icon="radix-icons:check" class="relative w-5 h-5 -top-px -left-px" />
</RadioGroupIndicator>
</RadioGroupItem>
{{ (option as RadioOption).label ?? option }}
</Label>
</RadioGroupRoot>
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
export interface RadioOption {
label: string
value: string
disabled?: boolean
}
const { disabled = false, options } = defineProps<{
disabled?: boolean
options: string[] | RadioOption[]
}>();
const model = defineModel<string>();
</script>

View File

@@ -0,0 +1,39 @@
<template>
<Label>{{ label }}
<SelectRoot :v-model="model">
<SelectTrigger :disabled="disabled" class="mx-4 inline-flex min-w-[160px] items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1
bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-none data-[placeholder]:font-normal
data-[placeholder]:text-light-50 dark:data-[placeholder]:text-dark-50 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
hover:border-light-50 dark:hover:border-dark-50">
<SelectValue :placeholder="placeholder" />
<Icon icon="radix-icons:caret-down" class="h-4 w-4" />
</SelectTrigger>
<SelectPortal :disabled="disabled">
<SelectContent :position="position"
class="min-w-[160px] bg-light-20 dark:bg-dark-20 will-change-[opacity,transform] z-40">
<SelectScrollUpButton>
<Icon icon="radix-icons:chevron-up" />
</SelectScrollUpButton>
<SelectViewport class="p-1">
<slot />
</SelectViewport>
<SelectScrollDownButton>
<Icon icon="radix-icons:chevron-down" />
</SelectScrollDownButton>
</SelectContent>
</SelectPortal>
</SelectRoot>
</Label>
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
const { placeholder, disabled = false, position = 'popper', label } = defineProps<{
placeholder?: string
disabled?: boolean
position?: 'item-aligned' | 'popper'
label?: string
}>();
const model = defineModel();
</script>

View File

@@ -0,0 +1,14 @@
<template>
<SelectGroup :disabled="disabled" class="">
<SelectLabel class="">{{ label }}</SelectLabel>
<slot />
</SelectGroup>
</template>
<script setup lang="ts">
import { SelectGroup } from 'radix-vue';
const { label, disabled = false } = defineProps<{
label: string
disabled?: boolean
}>();
</script>

View File

@@ -0,0 +1,18 @@
<template>
<SelectItem :value="value" :disabled="disabled" class="">
<SelectItemText class="">{{ label }}</SelectItemText>
<SelectItemIndicator class="">
<Icon icon="radix-icons:check" />
</SelectItemIndicator>
</SelectItem>
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
import { SelectItem } from 'radix-vue';
const { disabled = false, value } = defineProps<{
disabled?: boolean
value: NonNullable<any>
label: string
}>();
</script>

View File

@@ -0,0 +1,7 @@
<template>
<SelectSeparator class="" />
</template>
<script setup lang="ts">
import { SelectSeparator } from 'radix-vue';
</script>

View File

@@ -0,0 +1,25 @@
<template>
<Label class="flex justify-center items-center my-2">{{ label }}
<SliderRoot class="mx-4 relative flex items-center select-none touch-none w-[160px] h-5"
:default-value="model ? [model] : undefined" :v-model="[model]" :disabled="disabled"
@update:model-value="(value) => model = value ? value[0] : min" :min="min" :max="max" :step="step">
<SliderTrack class="bg-light-30 dark:bg-dark-30 relative h-1 w-full data-[disabled]:bg-light-10 dark:data-[disabled]:bg-dark-10">
<SliderRange class="absolute bg-light-40 dark:bg-dark-40 h-full data-[disabled]:bg-light-30 dark:data-[disabled]:bg-dark-30" />
</SliderTrack>
<SliderThumb
class="block w-5 h-5 bg-light-50 dark:bg-dark-50 outline-none focus:shadow-raw transition-[box-shadow] focus:shadow-light-60 dark:focus:shadow-dark-60 border border-light-50 dark:border-dark-50
hover:border-light-60 dark:hover:border-dark-60 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20" />
</SliderRoot>
</Label>
</template>
<script setup lang="ts">
const { min = 0, max = 100, step = 1, label, disabled = false } = defineProps<{
min?: number
max?: number
step?: number
label?: string
disabled?: boolean
}>();
const model = defineModel<number>()
</script>

View File

@@ -0,0 +1,22 @@
<template>
<Label class="flex justify-center items-center my-2">{{ label }}
<SwitchRoot v-model:checked="model" :disabled="disabled"
class="mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20">
<SwitchThumb
class="block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 data-[state=checked]:translate-x-[26px]
data-[disabled]:bg-light-30 dark:data-[disabled]:bg-dark-30 data-[disabled]:border-light-30 dark:data-[disabled]:border-dark-30" />
</SwitchRoot>
</Label>
</template>
<script setup lang="ts">
const { label, disabled, onIcon, offIcon } = defineProps<{
label?: string
disabled?: boolean
onIcon?: string
offIcon?: string
}>();
const model = defineModel<boolean>();
</script>

View File

@@ -0,0 +1,19 @@
<template>
<Label class="my-2">{{ label }}
<input :placeholder="placeholder" :disabled="disabled"
class="mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20"
:type="type" v-model="model" :data-disabled="disabled || undefined">
</Label>
</template>
<script setup lang="ts">
const { type = 'text', label, disabled = false, placeholder } = defineProps<{
type?: 'text' | 'password' | 'email' | 'tel' | 'url'
label?: string
disabled?: boolean
placeholder?: string
}>();
const model = defineModel<string>();
</script>

View File

@@ -0,0 +1,21 @@
<template>
<ProgressRoot class="my-2 relative overflow-hidden bg-light-25 dark:bg-dark-25 w-48 h-3 data-[shape=thin]:h-1 data-[shape=large]:h-6" :data-shape="shape" style="transform: translateZ(0)" >
<ProgressIndicator class="bg-light-50 dark:bg-dark-50 h-full w-0 transition-[width] ease-linear" :style="`transition-duration: ${delay}ms; width: ${progress ? 100 : 0}%`" />
</ProgressRoot>
</template>
<script setup lang="ts">
const { delay = 1500, decreasing = false, shape = 'normal' } = defineProps<{
delay?: number
decreasing?: boolean
shape?: 'thin' | 'normal' | 'large'
}>();
const emit = defineEmits(['finish']);
const progress = ref(false);
nextTick(() => {
progress.value = true;
setTimeout(emit, delay, 'finish');
});
</script>

83
components/base/Toast.vue Normal file
View File

@@ -0,0 +1,83 @@
<template>
<ToastRoot :duration="duration" class="ToastRoot bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30" v-model:open="model">
<div class="grid grid-cols-8 p-3">
<ToastTitle v-if="title" class="font-semibold text-xl col-span-7 text-light-70 dark:text-dark-70" asChild><h4>{{ title }}</h4></ToastTitle>
<ToastClose v-if="closeable" aria-label="Close" class="text-2xl -translate-y-1/2 translate-x-1/2 cursor-pointer"><span aria-hidden>×</span></ToastClose>
<ToastDescription v-if="content" class="text-md col-span-8 text-light-70 dark:text-dark-70" asChild><span>{{ content }}</span></ToastDescription>
</div>
<TimerProgress v-if="timer" shape="thin" :delay="duration" class="mb-0 mt-0 w-full" />
</ToastRoot>
<ToastViewport class="fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72" />
</template>
<script setup lang="ts">
const model = defineModel<boolean>();
const { closeable = true, duration, title, content, timer = true } = defineProps<{
closeable?: boolean
duration?: number
title?: string
content?: string
timer?: boolean
}>();
const timeout = ref<NodeJS.Timeout>();
watch(model, (value) => {
if(duration)
{
if(value === true)
{
timeout.value = setTimeout(() => model.value = false, duration);
}
else
{
clearTimeout(timeout.value);
}
}
})
</script>
<style>
.ToastRoot[data-state='open'] {
animation: slideIn .15s cubic-bezier(0.16, 1, 0.3, 1);
}
.ToastRoot[data-state='closed'] {
animation: hide .1s ease-in;
}
.ToastRoot[data-swipe='move'] {
transform: translateX(var(--radix-toast-swipe-move-x));
}
.ToastRoot[data-swipe='cancel'] {
transform: translateX(0);
transition: transform .2s ease-out;
}
.ToastRoot[data-swipe='end'] {
animation: swipeRight .1s ease-out;
}
@keyframes hide {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes slideIn {
from {
transform: translateX(calc(100% + var(--viewport-padding)));
}
to {
transform: translateX(0);
}
}
@keyframes swipeRight {
from {
transform: translateX(var(--radix-toast-swipe-end-x));
}
to {
transform: translateX(100%);
}
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<TooltipRoot :delay-duration="delay" :disabled="disabled">
<TooltipTrigger asChild>
<span tabindex="0"><slot></slot></span>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent class="TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70" :side="side" :side-offset="['left', 'right'].includes(side ?? '') ? 8 : 0">
{{ message }}
<TooltipArrow class="fill-light-30 dark:fill-dark-30"></TooltipArrow>
</TooltipContent>
</TooltipPortal>
</TooltipRoot>
</template>
<script setup lang="ts">
const { message, delay = 300, side } = defineProps<{
message: string
delay?: number,
disabled?: boolean
side?: 'left' | 'right' | 'top' | 'bottom'
}>();
</script>
<style>
.TooltipContent {
animation-duration: .3s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
.TooltipContent[data-side="top"] {
animation-name: slideUp;
}
.TooltipContent[data-side="bottom"] {
animation-name: slideDown;
}
.TooltipContent[data-side="left"] {
animation-name: slideLeft;
}
.TooltipContent[data-side="right"] {
animation-name: slideRight;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideRight {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideLeft {
from {
opacity: 0;
transform: translateX(10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
</style>