@monkeyplus/payscope 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,131 @@
1
+ interface TypeTax {
2
+ /**
3
+ * Codigo del typo de impuesto
4
+ * ej.
5
+ */
6
+ code: string;
7
+ /**
8
+ * Nombre del impuesto
9
+ * ej. IVA,ICE
10
+ */
11
+ name: string;
12
+ }
13
+ interface TaxItem {
14
+ /**
15
+ * Valor de impuesto en porcentaje
16
+ */
17
+ value: string | number;
18
+ /**
19
+ * Codigo de impuesto
20
+ */
21
+ code: string;
22
+ /**
23
+ * Descripcion del impuesto
24
+ */
25
+ description: string;
26
+ /**
27
+ * Impuesto incluido
28
+ */
29
+ included: boolean;
30
+ /**
31
+ * Calcular impuesto antes de descuento
32
+ */
33
+ beforeTaxes?: boolean;
34
+ /**
35
+ *
36
+ */
37
+ excludeDiscount?: boolean;
38
+ /**
39
+ * Tipo de impuesto
40
+ */
41
+ type: TypeTax;
42
+ }
43
+ interface TaxItemFull extends TaxItem {
44
+ amount: number;
45
+ totalAmount: number;
46
+ base: number;
47
+ totalBase: number;
48
+ }
49
+ type Discount = {
50
+ percent: string | number;
51
+ } | {
52
+ amount: string | number;
53
+ };
54
+ interface ItemTable {
55
+ id: any;
56
+ title: string;
57
+ taxes: TaxItem[];
58
+ taxIncluded: boolean;
59
+ quantity: string | number;
60
+ price: string | number;
61
+ /**
62
+ * Valor del descuento en porcentaje
63
+ */
64
+ discount: Discount;
65
+ product?: any;
66
+ productId?: any;
67
+ variant?: any;
68
+ discountAllocations?: any[];
69
+ }
70
+ interface ItemDiscount {
71
+ amount: number;
72
+ percent?: number;
73
+ }
74
+ interface Amount {
75
+ price: number;
76
+ unitAmount: number;
77
+ unitBase: number;
78
+ discounts: {
79
+ itemDiscount: ItemDiscount;
80
+ parentDiscount: ItemDiscount;
81
+ };
82
+ total?: number;
83
+ }
84
+ interface Item {
85
+ id: any;
86
+ title: string;
87
+ taxIncluded: boolean;
88
+ item: Amount;
89
+ taxes: TaxItemFull[];
90
+ variant: {
91
+ id: any;
92
+ [key: string]: any;
93
+ };
94
+ product: {
95
+ id: any;
96
+ [key: string]: any;
97
+ };
98
+ discountAllocations?: any[];
99
+ quantity: number;
100
+ productId?: any;
101
+ category?: string;
102
+ }
103
+ interface Invoice {
104
+ total: number;
105
+ base: number;
106
+ items: Item[];
107
+ lineItemsSubtotalPrice: number;
108
+ shipping: number;
109
+ totalTax: number;
110
+ totalDiscount: number;
111
+ taxes: {
112
+ /**
113
+ * Codigo de tipo de impuesto
114
+ */
115
+ type: string;
116
+ /**
117
+ * Codigo de impuesto
118
+ */
119
+ code: string;
120
+ base: number;
121
+ value: number;
122
+ rate: number;
123
+ title?: string;
124
+ }[];
125
+ }
126
+ declare function calcAmount(_itemDiscount: Discount, _parentDiscount: Discount, taxIncluded?: boolean): (price: number) => Amount;
127
+ declare function calcSingleItem(parentDiscount?: Discount): (item: ItemTable) => Item;
128
+ declare function calcItems(items: ItemTable[], parentDiscount?: Discount): Item[];
129
+ declare function buildInvoice(items: Item[]): Invoice;
130
+ declare function calculateFormula(expression: string, variables: Record<string, number>): number;
131
+ export { Amount, Discount, Invoice, Item, ItemDiscount, ItemTable, TaxItem, TaxItemFull, TypeTax, buildInvoice, calcAmount, calcItems, calcSingleItem, calculateFormula };
@@ -0,0 +1,2 @@
1
+ import { buildInvoice, calcAmount, calcItems, calcSingleItem, calculateFormula } from "../_chunks/taxes.mjs";
2
+ export { buildInvoice, calcAmount, calcItems, calcSingleItem, calculateFormula };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@monkeyplus/payscope",
3
3
  "type": "module",
4
- "version": "1.0.2",
4
+ "version": "1.0.3",
5
5
  "packageManager": "pnpm@10.8.1",
6
6
  "author": "",
7
7
  "keywords": [],
@@ -3,24 +3,15 @@ import { ref } from 'vue';
3
3
  import { useCartStore } from '../stores';
4
4
  import ShoppinCartItem from './ShoppinCartItem.vue';
5
5
 
6
- const toast = useToast();
6
+ // const toast = useToast();
7
7
  const cart = useCartStore();
8
8
  const loading = ref(false);
9
- function onOpen() {
10
- console.log('toast');
11
- cart.drawer = !cart.drawer;
12
- // toast.add({
13
- // title: 'Event added to calendar',
14
- // description: `This event is scheduled.`,
15
- // icon: 'i-lucide-calendar-days',
16
- // });
17
- }
18
9
  </script>
19
10
 
20
11
  <template>
21
12
  <!-- <UApp> -->
22
- <UDrawer direction="right">
23
- <UButton icon="i-bytesize-cart" size="xl" variant="ghost" class="mx-3" @click="onOpen" />
13
+ <UDrawer v-model:open="cart.open" direction="right">
14
+ <UButton icon="i-bytesize-cart" size="xl" variant="ghost" class="mx-3" />
24
15
 
25
16
  <template #content>
26
17
  <div
@@ -55,17 +55,19 @@ function onUpdate(id: string): void {
55
55
  {{ item?.variant?.price }}
56
56
  </div>
57
57
  </div>
58
- <template
59
- v-for="(option) in item?.variant?.selectedOptions || []"
60
- :key="option.name"
61
- >
62
- <div v-show="option.name !== 'x'" class="text-gray-900">
63
- <span class="font-bold"> {{ option.name }}: </span>
64
- <span>
65
- {{ option.value }}
66
- </span>
67
- </div>
68
- </template>
58
+ <div v-if="!item?.noVariants">
59
+ <template
60
+ v-for="(option) in item?.variant?.selectedOptions || []"
61
+ :key="option.name"
62
+ >
63
+ <div v-show="option.name !== 'x'" class="text-gray-900">
64
+ <span class="font-bold"> {{ option.name }}: </span>
65
+ <span>
66
+ {{ option.value }}
67
+ </span>
68
+ </div>
69
+ </template>
70
+ </div>
69
71
  <div class="flex-auto" />
70
72
  <div class="text-gray-800 pt-1">
71
73
  <div v-show="item.variant?.available">
@@ -11,7 +11,7 @@ const open = ref(true);
11
11
 
12
12
  <template>
13
13
  <UApp>
14
- <div class=" mx-auto h-screen flex flex-col ">
14
+ <div class=" mx-auto min-h-screen flex flex-col ">
15
15
  <div class="border-b border-gray-300">
16
16
  Header {{ checkout.isReady }}
17
17
  </div>
@@ -15,23 +15,21 @@ const checkout = useCheckoutStore();
15
15
 
16
16
  <template>
17
17
  <div>
18
- <div v-for="item in checkout.items" :key="item.id" class="flex items-center w-full">
19
- <div
20
- class="
21
- border-1
22
- rounded
23
- border-gray-200
24
- overflow-hidden overflow-visible
25
- "
26
- >
18
+ <h3 class="text-xl font-bold mb-4 text-gray-800">
19
+ Resumen de tu pedido
20
+ </h3>
21
+ <div v-for="item in checkout.items" :key="item.id" class="flex items-center w-full gap-4">
22
+ <div>
27
23
  <div class="relative">
28
- <div
29
- class=" h-[4.5rem] w-[4.5rem] min-w-[4.5rem] bg-gray-50 flex justify-center items-center"
30
- >
24
+ <div class="w-16 h-16 rounded-md overflow-hidden bg-white border border-gray-200 flex-shrink-0">
31
25
  <img
26
+ v-if="item?.image?.src"
32
27
  :src="item?.image?.src"
33
28
  alt="h-full w-full object-containt"
34
29
  >
30
+ <div v-else class="w-full h-full flex items-center justify-center text-gray-400">
31
+ <i-mdi-image-outline class="text-2xl" />
32
+ </div>
35
33
  </div>
36
34
  <div
37
35
  class="
@@ -50,20 +48,29 @@ const checkout = useCheckoutStore();
50
48
  </div>
51
49
  </div>
52
50
  </div>
53
- <div class="flex-auto px-3">
54
- {{ item.title }}
55
- <div
56
- v-for="(option) in item.options"
57
- :key="option.name"
58
- class="text-xs"
59
- >
60
- <span class="font-bold"> {{ option.name }}: </span>
61
- <span>
62
- {{ option.value }}
63
- </span>
51
+ <div class="flex-1 flex justify-between">
52
+ <div>
53
+ <div class="font-medium text-gray-800 text-sm leading-tight">
54
+ {{ item.title }}
55
+ </div>
56
+ <div
57
+ v-for="(option) in item.options"
58
+ :key="option.name"
59
+ class="text-xs text-gray-500 mt-1"
60
+ >
61
+ <span class="font-bold"> {{ option.name }}: </span>
62
+ <span>
63
+ {{ option.value }}
64
+ </span>
65
+ </div>
66
+ <div class="text-xs text-gray-400 mt-1">
67
+ Cant: {{ item.quantity }}
68
+ </div>
69
+ </div>
70
+ <div class="font-bold text-gray-800 text-sm">
71
+ $ {{ item.total }}
64
72
  </div>
65
73
  </div>
66
- <div>$ {{ item.total }}</div>
67
74
  </div>
68
75
  <div class="h-[2px] bg-gray-200 my-8" />
69
76
  <AppCartDiscount />
@@ -11,7 +11,7 @@ const taxIncluded = ref(false);
11
11
  <div class="pt-3 space-y-1">
12
12
  <div v-if="checkout.invoice?.discount || checkout.invoice?.discounts?.length" class="border-b mb-2 pb-1">
13
13
  <div class="flex">
14
- <div>
14
+ <div class="text-gray-500">
15
15
  Total
16
16
  <span class="text-xs">(Sin descuentos)</span>
17
17
  </div>
@@ -27,45 +27,49 @@ const taxIncluded = ref(false);
27
27
  </div>
28
28
  </div>
29
29
  <div class="flex">
30
- <div>Subtotal</div>
30
+ <div class="text-gray-500 text-sm">
31
+ Subtotal
32
+ </div>
31
33
  <div class="flex-auto" />
32
- <div>
33
- {{ checkout.totals.base }}
34
+ <div class="font-medium">
35
+ ${{ checkout.totals.base }}
34
36
  </div>
35
37
  </div>
36
38
  <div class="flex">
37
- <div>
39
+ <div class="text-gray-500 text-sm">
38
40
  Impuestos
39
41
  </div>
40
42
  <div class="flex-auto" />
41
- <div v-if="!taxIncluded">
42
- {{ checkout.totals.totalTax }}
43
+ <div v-if="!taxIncluded" class="font-medium">
44
+ ${{ checkout.totals.totalTax }}
43
45
  </div>
44
46
  <div v-else class="italic text-sm">
45
47
  Ya incluidos
46
48
  </div>
47
49
  </div>
48
50
  <div class="flex">
49
- <div>Envio</div>
51
+ <div class="text-gray-500 text-sm">
52
+ Envio
53
+ </div>
50
54
  <div class="flex-auto" />
51
- <div>
52
- {{ checkout.totals.shipping }}
55
+ <div class="font-medium">
56
+ ${{ checkout.totals.shipping }}
53
57
  </div>
54
58
  </div>
55
59
  </div>
56
60
  <div class="border-b-1 border-black opacity-10 my-4 border" />
57
- <div class="flex">
61
+ <div class="flex items-center font-bold">
58
62
  <div>
59
- <div class="text-lg text-gray-700">
63
+ <div class=" text-gray-800">
60
64
  Total
61
65
  </div>
62
66
  </div>
63
67
  <div class="flex-auto" />
64
- <div class="text-2xl text-gray-800">
65
- <span class="text-sm">
68
+ <div class="text-xl text-gray-900">
69
+ <span class="text-sm text-gray-500">
66
70
  USD
67
71
  </span>
68
- {{ checkout.totals.total }} $
72
+ ${{ checkout.totals.total }}
69
73
  </div>
70
74
  </div>
71
75
  </div>
@@ -0,0 +1,8 @@
1
+ <script setup lang="ts">
2
+ </script>
3
+
4
+ <template>
5
+ <div>
6
+ <router-view />
7
+ </div>
8
+ </template>
@@ -0,0 +1,95 @@
1
+ <script setup lang="ts">
2
+ import { reactive, ref } from 'vue';
3
+
4
+ const props = defineProps<{
5
+ step: number;
6
+ isActive: boolean;
7
+ }>();
8
+
9
+ const emit = defineEmits(['next', 'edit']);
10
+
11
+ const requireBilling = ref(false);
12
+
13
+ const form = reactive({
14
+ rfc: '',
15
+ name: '',
16
+ address: ''
17
+ });
18
+
19
+ const isComplete = ref(false);
20
+
21
+ function submit() {
22
+ if (!requireBilling.value) {
23
+ isComplete.value = true;
24
+ emit('next');
25
+ return;
26
+ }
27
+ if (form.rfc && form.name) {
28
+ isComplete.value = true;
29
+ emit('next');
30
+ }
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <div class="bg-white p-6 rounded-xl border shadow-sm transition-colors duration-300" :class="isActive ? 'border-primary' : 'border-gray-100'">
36
+ <div class="flex items-center justify-between mb-2">
37
+ <div class="flex items-center space-x-3">
38
+ <div class="w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm transition-colors" :class="isActive ? 'bg-primary text-white' : (isComplete ? 'bg-green-500 text-white' : 'bg-slate-100 text-slate-600')">
39
+ <i-mdi-check v-if="!isActive && isComplete" class="text-lg" />
40
+ <span v-else>{{ step }}</span>
41
+ </div>
42
+ <h3 class="text-xl font-bold text-gray-800">
43
+ Datos de Facturación
44
+ </h3>
45
+ </div>
46
+ <div class="flex items-center space-x-2" v-if="isActive">
47
+ <span class="text-sm text-gray-500 font-medium">Requerir factura</span>
48
+ <UToggle v-model="requireBilling" color="primary" />
49
+ </div>
50
+ <UButton v-else-if="isComplete" variant="ghost" color="gray" icon="i-heroicons-pencil-square" size="sm" @click="$emit('edit')">Editar</UButton>
51
+ </div>
52
+
53
+ <div class="pl-11 pt-2 transition-all">
54
+ <div v-if="isActive" class="animate-fade-in">
55
+ <form @submit.prevent="submit" class="space-y-4">
56
+ <div v-if="requireBilling" class="animate-fade-in space-y-4 mt-2">
57
+ <UFormField label="RFC / Identificación" required>
58
+ <UInput v-model="form.rfc" placeholder="Ej. XAXX010101000" />
59
+ </UFormField>
60
+ <UFormField label="Razón Social / Nombre" required>
61
+ <UInput v-model="form.name" placeholder="Razón Social" />
62
+ </UFormField>
63
+ <UFormField label="Dirección Fiscal">
64
+ <UInput v-model="form.address" placeholder="Dirección completa" />
65
+ </UFormField>
66
+ </div>
67
+ <div v-else class="text-sm text-gray-500 italic mb-4 mt-2">
68
+ Has seleccionado que no requieres factura.
69
+ </div>
70
+ <div class="pt-4 flex justify-end">
71
+ <UButton type="submit" size="lg">Guardar y Continuar</UButton>
72
+ </div>
73
+ </form>
74
+ </div>
75
+ <div v-else-if="isComplete" class="text-sm text-gray-600 space-y-1 animate-fade-in">
76
+ <template v-if="requireBilling">
77
+ <p class="font-medium text-gray-900">{{ form.name }}</p>
78
+ <p>RFC: {{ form.rfc }}</p>
79
+ <p v-if="form.address">{{ form.address }}</p>
80
+ </template>
81
+ <p v-else class="italic">No se requiere factura</p>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </template>
86
+
87
+ <style scoped>
88
+ .animate-fade-in {
89
+ animation: fadeIn 0.3s ease-in-out;
90
+ }
91
+ @keyframes fadeIn {
92
+ from { opacity: 0; transform: translateY(-5px); }
93
+ to { opacity: 1; transform: translateY(0); }
94
+ }
95
+ </style>
@@ -0,0 +1,188 @@
1
+ <script setup lang="ts">
2
+ import { useAsyncState } from '@vueuse/core';
3
+ import { vMaska } from 'maska/vue';
4
+ import { ofetch } from 'ofetch';
5
+ import { computed, reactive, ref, watch } from 'vue';
6
+
7
+ defineProps<{
8
+ step: number
9
+ isActive: boolean
10
+ }>();
11
+
12
+ const emit = defineEmits(['next', 'edit']);
13
+
14
+ const form = reactive({
15
+ firstName: '',
16
+ lastName: '',
17
+ email: '',
18
+ phone: '',
19
+ });
20
+
21
+ interface PhoneCode {
22
+ name: string
23
+ code: string
24
+ emoji: string
25
+ dialCode: string
26
+ mask: string
27
+ }
28
+
29
+ const countryCode = ref('EC'); // Default country (Example: Mexico or US)
30
+
31
+ // Adapted from useLazyFetch to useAsyncState since we are in a Vue3/Vite env
32
+ const { state: phoneCodes, isLoading: statusPending, execute } = useAsyncState(async () => {
33
+ return await ofetch<PhoneCode[]>('/api/phone-codes.json').catch(() => {
34
+ // Fallback data in case the endpoint doesn't exist
35
+ return [
36
+ { name: 'United States', code: 'US', emoji: '🇺🇸', dialCode: '+1', mask: '(###) ###-####' },
37
+ { name: 'Mexico', code: 'MX', emoji: '🇲🇽', dialCode: '+52', mask: '## #### ####' },
38
+ { name: 'Spain', code: 'ES', emoji: '🇪🇸', dialCode: '+34', mask: '### ### ###' },
39
+ { name: 'Colombia', code: 'CO', emoji: '🇨🇴', dialCode: '+57', mask: '### ### ####' },
40
+ { name: 'Argentina', code: 'AR', emoji: '🇦🇷', dialCode: '+54', mask: '## #### ####' },
41
+ { name: 'Ecuador', code: 'EC', emoji: '🇪🇨', dialCode: '+593', mask: '### ### ###' },
42
+ ];
43
+ });
44
+ }, [], {
45
+ immediate: false,
46
+ });
47
+
48
+ const country = computed(() => phoneCodes.value?.find(c => c.code === countryCode.value));
49
+ const dialCode = computed(() => country.value?.dialCode || '+52');
50
+ const mask = computed(() => country.value?.mask || '## #### ####');
51
+
52
+ function onOpen() {
53
+ if (!phoneCodes.value?.length) {
54
+ execute();
55
+ }
56
+ }
57
+
58
+ watch(countryCode, () => {
59
+ form.phone = '';
60
+ });
61
+
62
+ const isComplete = ref(false);
63
+
64
+ function submit() {
65
+ if (form.firstName && form.email) {
66
+ isComplete.value = true;
67
+ // Here you would normally commit this info to the store
68
+ emit('next');
69
+ }
70
+ }
71
+ </script>
72
+
73
+ <template>
74
+ <div class="bg-white p-6 rounded-xl border shadow-sm transition-colors duration-300" :class="isActive ? 'border-primary' : 'border-gray-100'">
75
+ <div class="flex items-center justify-between mb-2">
76
+ <div class="flex items-center space-x-3">
77
+ <div class="w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm transition-colors" :class="isActive ? 'bg-primary text-white' : (isComplete ? 'bg-green-500 text-white' : 'bg-slate-100 text-slate-600')">
78
+ <i-mdi-check v-if="!isActive && isComplete" class="text-lg" />
79
+ <span v-else>{{ step }}</span>
80
+ </div>
81
+ <h3 class="text-xl font-bold text-gray-800">
82
+ Información de Contacto
83
+ </h3>
84
+ </div>
85
+ <UButton v-if="!isActive && isComplete" variant="ghost" color="gray" icon="i-heroicons-pencil-square" size="sm" @click="$emit('edit')">
86
+ Editar
87
+ </UButton>
88
+ </div>
89
+
90
+ <div class="pl-11 pt-2 transition-all">
91
+ <div v-if="isActive" class="animate-fade-in">
92
+ <form class="space-y-4" @submit.prevent="submit">
93
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
94
+ <UFormField label="Nombre" required>
95
+ <UInput v-model="form.firstName" placeholder="Tu nombre" />
96
+ </UFormField>
97
+ <UFormField label="Apellido">
98
+ <UInput v-model="form.lastName" placeholder="Tu apellido" />
99
+ </UFormField>
100
+ </div>
101
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
102
+ <UFormField label="Correo Electrónico" required>
103
+ <UInput v-model="form.email" type="email" placeholder="correo@ejemplo.com" />
104
+ </UFormField>
105
+ <UFormField label="Teléfono">
106
+ <div class="flex">
107
+ <USelectMenu
108
+ v-model="countryCode"
109
+ :items="phoneCodes"
110
+ value-key="code"
111
+ :search-input="{
112
+ placeholder: 'Search country...',
113
+ icon: 'i-lucide-search',
114
+ loading: statusPending,
115
+ }"
116
+ :filter-fields="['name', 'code', 'dialCode']"
117
+ :content="{ align: 'start' }"
118
+ :ui="{
119
+ base: 'pe-8 rounded-r-none border-r-0 focus:z-10 bg-gray-50',
120
+ content: 'w-48',
121
+ placeholder: 'hidden',
122
+ trailingIcon: 'size-4',
123
+ }"
124
+ trailing-icon="i-lucide-chevrons-up-down"
125
+ @update:open="onOpen"
126
+ >
127
+ <span class="size-5 flex items-center text-lg">
128
+ {{ country?.emoji || '\u{1F1FA}\u{1F1F8}' }}
129
+ </span>
130
+
131
+ <template #item-leading="{ item }">
132
+ <span class="size-5 flex items-center text-lg">
133
+ {{ item.emoji }}
134
+ </span>
135
+ </template>
136
+
137
+ <template #item-label="{ item }">
138
+ {{ item.name }} ({{ item.dialCode }})
139
+ </template>
140
+ </USelectMenu>
141
+
142
+ <UInput
143
+ v-model="form.phone"
144
+ v-maska="mask"
145
+ class="flex-1"
146
+ :placeholder="mask.replaceAll('#', '_')"
147
+ :style="{ '--dial-code-length': `${dialCode.length + 1.5}ch` }"
148
+ :ui="{
149
+ base: 'ps-[var(--dial-code-length)] rounded-l-none',
150
+ leading: 'pointer-events-none text-base md:text-sm text-gray-400 font-medium',
151
+ }"
152
+ >
153
+ <template #leading>
154
+ {{ dialCode }}
155
+ </template>
156
+ </UInput>
157
+ </div>
158
+ </UFormField>
159
+ </div>
160
+ <div class="pt-4 flex justify-end">
161
+ <UButton type="submit" size="lg">
162
+ Guardar y Continuar
163
+ </UButton>
164
+ </div>
165
+ </form>
166
+ </div>
167
+ <div v-else-if="isComplete" class="text-sm text-gray-600 space-y-1 animate-fade-in">
168
+ <p class="font-medium text-gray-900">
169
+ {{ form.firstName }} {{ form.lastName }}
170
+ </p>
171
+ <p>{{ form.email }}</p>
172
+ <p v-if="form.phone">
173
+ {{ form.phone }}
174
+ </p>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </template>
179
+
180
+ <style scoped>
181
+ .animate-fade-in {
182
+ animation: fadeIn 0.3s ease-in-out;
183
+ }
184
+ @keyframes fadeIn {
185
+ from { opacity: 0; transform: translateY(-5px); }
186
+ to { opacity: 1; transform: translateY(0); }
187
+ }
188
+ </style>