@fy-/fws-vue 0.0.1

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,243 @@
1
+ <script setup lang="ts">
2
+ import { LinkIcon } from "@heroicons/vue/24/solid";
3
+ import { computed, ref, toRef } from "vue";
4
+ import type { ErrorObject } from "@vuelidate/core";
5
+ import { useTranslation } from "../../translations";
6
+ type modelValueType = string | number | string[] | undefined;
7
+ type checkboxValueType = any[] | Set<any> | undefined | boolean;
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ id: string;
11
+ showLabel?: boolean;
12
+ label?: string;
13
+ type?: string;
14
+ placeholder?: string;
15
+ autocomplete?: string;
16
+ checkboxTrueValue?: string | boolean;
17
+ checkboxFalseValue?: string | boolean;
18
+ req?: boolean;
19
+ linkIcon?: string;
20
+ modelValue?: modelValueType;
21
+ checkboxValue?: checkboxValueType;
22
+ options?: string[][];
23
+ help?: string;
24
+ error?: string;
25
+ errorVuelidate?: ErrorObject[];
26
+ disabled?: boolean;
27
+ }>(),
28
+ {
29
+ showLabel: true,
30
+ type: "text",
31
+ req: false,
32
+ options: () => [],
33
+ checkboxTrueValue: true,
34
+ checkboxFalseValue: false,
35
+ disabled: false,
36
+ },
37
+ );
38
+ const translate = useTranslation();
39
+ const inputRef = ref<HTMLInputElement>();
40
+ const errorProps = toRef(props, "error");
41
+ const errorVuelidateProps = toRef(props, "errorVuelidate");
42
+
43
+ const checkErrors = computed(() => {
44
+ if (errorProps.value) return errorProps.value;
45
+ if (errorVuelidateProps.value && errorVuelidateProps.value.length > 0) {
46
+ const err = `vuelidate_validator_${errorVuelidateProps.value[0].$validator.toString()}`;
47
+ return translate(err);
48
+ }
49
+
50
+ return null;
51
+ });
52
+
53
+ const focus = () => {
54
+ if (inputRef.value) inputRef.value.focus();
55
+ };
56
+ const getInputRef = () => {
57
+ if (inputRef.value) return inputRef.value;
58
+ };
59
+
60
+ const emit = defineEmits(["update:modelValue", "update:checkboxValue"]);
61
+ const model = computed({
62
+ get: () => props.modelValue,
63
+ set: (items) => {
64
+ emit("update:modelValue", items);
65
+ },
66
+ });
67
+ const modelCheckbox = computed({
68
+ get: () => props.checkboxValue,
69
+ set: (items) => {
70
+ emit("update:checkboxValue", items);
71
+ },
72
+ });
73
+ defineExpose({ focus, getInputRef });
74
+ </script>
75
+ <template>
76
+ <div>
77
+ <template
78
+ v-if="
79
+ [
80
+ 'text',
81
+ 'password',
82
+ 'email',
83
+ 'search',
84
+ 'date',
85
+ 'datetime',
86
+ 'url',
87
+ 'textarea',
88
+ 'select',
89
+ 'phone',
90
+ ].includes(type)
91
+ "
92
+ >
93
+ <label
94
+ :for="id"
95
+ v-if="label"
96
+ class="fws-label"
97
+ :class="{
98
+ error: checkErrors,
99
+ }"
100
+ >
101
+ {{ label }} <span v-if="req" class="text-red-700">*</span>
102
+ </label>
103
+ <input
104
+ v-if="
105
+ [
106
+ 'text',
107
+ 'password',
108
+ 'email',
109
+ 'search',
110
+ 'date',
111
+ 'datetime',
112
+ 'url',
113
+ ].includes(type)
114
+ "
115
+ class="fws-input"
116
+ :class="{
117
+ error: checkErrors,
118
+ }"
119
+ ref="inputRef"
120
+ :aria-describedby="label"
121
+ :autocomplete="autocomplete"
122
+ :id="id"
123
+ v-model="model"
124
+ :type="type"
125
+ :disabled="disabled"
126
+ :required="req"
127
+ :placeholder="placeholder"
128
+ />
129
+ <textarea
130
+ :aria-describedby="label"
131
+ ref="inputRef"
132
+ v-if="type == 'textarea'"
133
+ class="fws-textarea"
134
+ :class="{
135
+ error: checkErrors,
136
+ }"
137
+ :autocomplete="autocomplete"
138
+ :id="id"
139
+ v-model="model"
140
+ :disabled="disabled"
141
+ :required="req"
142
+ :placeholder="placeholder"
143
+ ></textarea>
144
+ <select
145
+ :aria-describedby="label"
146
+ :disabled="disabled"
147
+ v-if="type == 'select'"
148
+ :required="req"
149
+ v-model="model"
150
+ :id="id"
151
+ ref="inputRef"
152
+ :class="{
153
+ error: checkErrors,
154
+ }"
155
+ class="fws-select"
156
+ >
157
+ <option v-for="opt in options" :value="opt[0]" :key="opt[0].toString()">
158
+ {{ opt[1] }}
159
+ </option>
160
+ </select>
161
+ </template>
162
+
163
+ <template v-if="type == 'checkbox'">
164
+ <div class="fws-checkbox">
165
+ <input
166
+ :aria-describedby="label"
167
+ :id="id"
168
+ ref="inputRef"
169
+ :true-value="checkboxTrueValue"
170
+ :false-value="checkboxFalseValue"
171
+ v-model="modelCheckbox"
172
+ type="checkbox"
173
+ value=""
174
+ />
175
+ <label
176
+ :for="id"
177
+ v-if="label"
178
+ :class="{
179
+ error: checkErrors,
180
+ }"
181
+ >{{ label }}
182
+ <a class="fws-link" :href="linkIcon" target="_blank" v-if="linkIcon">
183
+ <LinkIcon class="w-4 h-4" />
184
+ </a>
185
+ </label>
186
+ </div>
187
+ </template>
188
+ <p v-if="checkErrors" class="mt-2 text-xs fws-error-text">
189
+ {{ checkErrors }}
190
+ </p>
191
+
192
+ <p
193
+ class="fws-input-helper"
194
+ v-if="help && !['checkbox', 'radio'].includes(type)"
195
+ >
196
+ {{ help }}
197
+ </p>
198
+ </div>
199
+ </template>
200
+ <style scoped>
201
+ .fws-label {
202
+ @apply block mb-2 text-sm font-medium text-fv-neutral-900 dark:text-white;
203
+
204
+ &.error {
205
+ @apply text-red-700 dark:text-red-500;
206
+ }
207
+ }
208
+ .fws-input-helper {
209
+ @apply mt-2 text-sm text-fv-neutral-500 dark:text-fv-neutral-400;
210
+ }
211
+ .fws-input {
212
+ @apply shadow-sm bg-fv-neutral-50 border border-fv-neutral-300 text-fv-neutral-900 text-sm rounded-lg focus:ring-fv-primary-500 focus:border-fv-primary-500 block w-full p-2.5 dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-400 dark:text-white dark:focus:ring-fv-primary-500 dark:focus:border-fv-primary-500;
213
+ &.error {
214
+ @apply bg-red-50 border border-red-500 text-red-900 placeholder-red-700 text-sm rounded-lg focus:ring-red-500 focus:border-red-500 block w-full p-2.5 dark:bg-red-100 dark:border-red-400;
215
+ }
216
+ }
217
+ .fws-textarea {
218
+ @apply block p-2.5 w-full text-sm text-fv-neutral-900 bg-fv-neutral-50 rounded-lg border border-fv-neutral-300 focus:ring-fv-primary-500 focus:border-fv-primary-500 dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-400 dark:text-white dark:focus:ring-fv-primary-500 dark:focus:border-fv-primary-500;
219
+ &.error {
220
+ @apply bg-red-50 border border-red-500 text-red-900 placeholder-red-700 text-sm rounded-lg focus:ring-red-500 focus:border-red-500 block w-full p-2.5 dark:bg-red-100 dark:border-red-400;
221
+ }
222
+ }
223
+
224
+ .fws-select {
225
+ @apply bg-fv-neutral-50 border border-fv-neutral-300 text-fv-neutral-900 text-sm rounded-lg focus:ring-fv-primary-500 focus:border-fv-primary-500 block w-full p-2.5 dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-400 dark:text-white dark:focus:ring-fv-primary-500 dark:focus:border-fv-primary-500;
226
+ &.error {
227
+ @apply bg-red-50 border border-red-500 text-red-900 placeholder-red-700 text-sm rounded-lg focus:ring-red-500 focus:border-red-500 block w-full p-2.5 dark:bg-red-100 dark:border-red-400;
228
+ }
229
+ }
230
+ .fws-checkbox {
231
+ @apply flex items-center mb-4;
232
+ }
233
+
234
+ .fws-checkbox input {
235
+ @apply w-4 h-4 text-fv-primary-600 bg-fv-neutral-100 border-fv-neutral-300 rounded focus:ring-fv-primary-500 dark:focus:ring-fv-primary-600 dark:ring-offset-fv-neutral-800 dark:focus:ring-offset-fv-neutral-800 focus:ring-2 dark:bg-fv-neutral-700 dark:border-fv-neutral-600;
236
+ &.error {
237
+ @apply bg-red-50 border border-red-500 text-red-900 placeholder-red-700 text-sm rounded-lg focus:ring-red-500 focus:border-red-500 block w-full p-2.5 dark:bg-red-100 dark:border-red-400;
238
+ }
239
+ }
240
+ .fws-checkbox label {
241
+ @apply ml-2 text-sm font-medium text-fv-neutral-900 dark:text-fv-neutral-300;
242
+ }
243
+ </style>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, onUnmounted, ref } from "vue";
3
+ import type { Component } from "vue";
4
+ import { useEventBus } from "../../event-bus";
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ image: string;
9
+ force?: boolean;
10
+ id?: string;
11
+ }>(),
12
+ {
13
+ force: false,
14
+ id: "",
15
+ },
16
+ );
17
+ const eventBus = useEventBus();
18
+ const loading = ref<boolean>(false);
19
+ function setLoading(value: boolean) {
20
+ loading.value = value;
21
+ }
22
+ onMounted(() => {
23
+ if (props.id) eventBus.on(`${props.id}-loading`, setLoading);
24
+ else eventBus.on("loading", setLoading);
25
+ });
26
+ onUnmounted(() => {
27
+ if (props.id) eventBus.off(`${props.id}-loading`, setLoading);
28
+ else eventBus.off("loading", setLoading);
29
+ });
30
+ </script>
31
+
32
+ <template>
33
+ <div
34
+ v-if="loading || force"
35
+ class="flex-grow flex flex-col bg-fv-neutral-800/[.8] items-center justify-center absolute inset-0 z-50"
36
+ >
37
+ <div class="text-center animate-pulse w-40 h-40 p-4 rounded-full shadow">
38
+ <div class="relative flex flex-col items-center">
39
+ <div class="flex items-center justify-center flex-col">
40
+ <img
41
+ :src="image"
42
+ :alt="$t('global_loading')"
43
+ class="w-full h-full relative"
44
+ />
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </template>
@@ -0,0 +1,90 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ Dialog,
4
+ DialogPanel,
5
+ DialogTitle,
6
+ TransitionRoot,
7
+ } from "@headlessui/vue";
8
+ import { ref, onMounted, onUnmounted, h } from "vue";
9
+ import { XCircleIcon } from "@heroicons/vue/24/solid";
10
+ import { useEventBus } from "../../event-bus";
11
+ const props = withDefaults(
12
+ defineProps<{
13
+ id: string;
14
+ title?: string;
15
+ onOpen?: Function;
16
+ onClose?: Function;
17
+ closeIcon?: Object;
18
+ }>(),
19
+ {
20
+ closeIcon: () => h(XCircleIcon),
21
+ },
22
+ );
23
+
24
+ const eventBus = useEventBus();
25
+
26
+ const isOpen = ref<boolean>(false);
27
+ const setModal = (value: boolean) => {
28
+ if (value === true) {
29
+ if (props.onOpen) props.onOpen();
30
+ } else {
31
+ if (props.onClose) props.onClose();
32
+ }
33
+ isOpen.value = value;
34
+ };
35
+ onMounted(() => {
36
+ eventBus.on(`${props.id}Modal`, setModal);
37
+ });
38
+ onUnmounted(() => {
39
+ eventBus.off(`${props.id}Modal`, setModal);
40
+ });
41
+ </script>
42
+ <template>
43
+ <TransitionRoot
44
+ :show="isOpen"
45
+ as="template"
46
+ enter="duration-300 ease-out"
47
+ enter-from="opacity-0"
48
+ enter-to="opacity-100"
49
+ leave="duration-200 ease-in"
50
+ leave-from="opacity-100"
51
+ leave-to="opacity-0"
52
+ >
53
+ <Dialog
54
+ :open="isOpen"
55
+ @close="setModal"
56
+ class="fixed inset-0 overflow-y-auto"
57
+ style="z-index: 40"
58
+ >
59
+ <DialogPanel
60
+ class="flex flex-col items-center justify-center min-h-screen text-fv-neutral-800 dark:text-fv-neutral-300 bg-fv-neutral-900/[.20] dark:bg-fv-neutral-50/[.20]"
61
+ style="z-index: 41"
62
+ >
63
+ <div
64
+ class="relative w-full max-w-2xl max-h-full bg-white rounded-lg shadow dark:bg-gray-700"
65
+ >
66
+ <div
67
+ class="flex items-center justify-between p-2 w-full border-b rounded-t dark:border-gray-600"
68
+ >
69
+ <slot name="before"></slot>
70
+ <DialogTitle
71
+ class="text-xl font-semibold text-gray-900 dark:text-white"
72
+ v-if="title"
73
+ >
74
+ {{ title }}
75
+ </DialogTitle>
76
+ <button
77
+ @click="setModal(false)"
78
+ class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
79
+ >
80
+ <component :is="closeIcon" class="w-7 h-7" />
81
+ </button>
82
+ </div>
83
+ <div class="p-3 space-y-3">
84
+ <slot></slot>
85
+ </div>
86
+ </div>
87
+ </DialogPanel>
88
+ </Dialog>
89
+ </TransitionRoot>
90
+ </template>
@@ -0,0 +1,212 @@
1
+ <script setup lang="ts">
2
+ import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/vue/24/solid";
3
+ import {
4
+ watch,
5
+ onUnmounted,
6
+ ref,
7
+ WatchStopHandle,
8
+ onMounted,
9
+ computed,
10
+ } from "vue";
11
+ import type { APIPaging } from "../../rest";
12
+ import { useEventBus } from "../../event-bus";
13
+ import { useServerRouter } from "../../stores/serverRouter";
14
+ import { useRoute } from "vue-router";
15
+ import { useFyHead } from "@fy-/head";
16
+ import { hasFW, getURL } from "@fy-/fws-js";
17
+ const props = defineProps<{
18
+ items: APIPaging;
19
+ id: string;
20
+ }>();
21
+ const route = useRoute();
22
+ const eventBus = useEventBus();
23
+ const history = useServerRouter();
24
+ const prevNextSeo = ref<any>({});
25
+ const isNewPage = (page: number) => {
26
+ return (
27
+ page >= 1 && page <= props.items.page_max && page != props.items.page_no
28
+ );
29
+ };
30
+ const pageWatcher = ref<WatchStopHandle>();
31
+
32
+ const next = () => {
33
+ const page = props.items.page_no + 1;
34
+
35
+ if (!isNewPage(page)) return;
36
+
37
+ history.push({
38
+ path: history.currentRoute.path,
39
+ query: { page: page.toString() },
40
+ });
41
+ };
42
+ const prev = () => {
43
+ const page = props.items.page_no - 1;
44
+ if (!isNewPage(page)) return;
45
+
46
+ history.push({
47
+ path: history.currentRoute.path,
48
+ query: { page: page.toString() },
49
+ });
50
+ };
51
+ const page = (page: number) => {
52
+ if (!isNewPage(page)) return;
53
+
54
+ history.push({
55
+ path: history.currentRoute.path,
56
+ query: { page: page.toString() },
57
+ });
58
+ };
59
+
60
+ const checkPageNumber = (page: number = 1) => {
61
+ prevNextSeo.value.next = undefined;
62
+ prevNextSeo.value.prev = undefined;
63
+
64
+ if (hasFW()) {
65
+ const url = getURL();
66
+ if (page + 1 <= props.items.page_max && url) {
67
+ prevNextSeo.value.next = `${url.Scheme}://${url.Host}${url.Path}?page=${
68
+ page + 1
69
+ }`;
70
+ }
71
+ if (page - 1 >= 1 && url) {
72
+ prevNextSeo.value.prev = `${url.Scheme}://${url.Host}${url.Path}?page=${
73
+ page - 1
74
+ }`;
75
+ }
76
+ }
77
+ };
78
+
79
+ pageWatcher.value = watch(
80
+ () => route.query.page,
81
+ (v) => {
82
+ eventBus.emit(`${props.id}GoToPage`, v ? v : 1);
83
+ },
84
+ );
85
+ onMounted(() => {
86
+ eventBus.on(`${props.id}GoToPage`, checkPageNumber);
87
+ });
88
+ onUnmounted(() => {
89
+ eventBus.off(`${props.id}GoToPage`, checkPageNumber);
90
+ //if (pageWatcher.value) pageWatcher.value();
91
+ });
92
+
93
+ checkPageNumber(props.items.page_no);
94
+ useFyHead({
95
+ links: computed(() => {
96
+ const result: any = [];
97
+ if (prevNextSeo.value.next)
98
+ result.push({
99
+ href: prevNextSeo.value.next,
100
+ rel: "next",
101
+ key: "next",
102
+ });
103
+ if (prevNextSeo.value.prev)
104
+ result.push({
105
+ href: prevNextSeo.value.prev,
106
+ rel: "prev",
107
+ key: "prev",
108
+ });
109
+ return result;
110
+ }),
111
+ });
112
+ </script>
113
+ <template>
114
+ <div
115
+ class="flex items-center justify-center"
116
+ v-if="items && items.page_max > 1 && items.page_no"
117
+ >
118
+ <div class="paging-container">
119
+ <nav
120
+ aria-label="Pagination"
121
+ class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px"
122
+ >
123
+ <a
124
+ href="javascript:void(0);"
125
+ @click="prev()"
126
+ v-if="items.page_no >= 2"
127
+ class="relative inline-flex items-center px-2 pt-2 pb-1.5 border text-sm font-medium border-fv-neutral-300 bg-fv-neutral-100 text-fv-neutral-500 hover:bg-fv-neutral-200 hover:text-fv-neutral-600 dark:border-fv-neutral-600 dark:bg-fv-neutral-800 dark:text-fv-neutral-200 dark:hover:bg-fv-neutral-800 dark:hover:text-fv-neutral-100"
128
+ >
129
+ <span class="sr-only">{{ $t("previous_paging") }}</span>
130
+ <ChevronLeftIcon class="w-4 h-4" />
131
+ </a>
132
+ <a
133
+ v-if="items.page_no - 2 > 1"
134
+ class="relative inline-flex items-center px-4 pt-2 pb-1.5 border text-sm font-medium bg-fv-neutral-100 border-fv-neutral-300 text-fv-neutral-500 hover:bg-fv-neutral-200 hover:text-fv-neutral-600 dark:bg-fv-neutral-800 dark:border-fv-neutral-600 dark:text-fv-neutral-200 dark:hover:bg-fv-neutral-800 dark:hover:text-fv-neutral-100"
135
+ href="javascript:void(0);"
136
+ @click="page(1)"
137
+ >
138
+ 1
139
+ </a>
140
+ <span
141
+ v-if="items.page_no - 2 > 2"
142
+ class="relative inline-flex items-center px-2 pt-2 pb-1.5 border text-sm font-medium border-fv-neutral-300 bg-fv-neutral-100 text-fv-neutral-700 hover:text-fv-neutral-600 dark:border-fv-neutral-600 dark:bg-fv-neutral-800 dark:text-fv-neutral-700 dark:hover:text-fv-neutral-100"
143
+ >
144
+ ...
145
+ </span>
146
+ <template v-for="i in 2">
147
+ <a
148
+ v-if="items.page_no - (3 - i) >= 1"
149
+ class="relative inline-flex items-center px-4 pt-2 pb-1.5 border text-sm font-medium bg-fv-neutral-100 border-fv-neutral-300 text-fv-neutral-500 hover:bg-fv-neutral-200 hover:text-fv-neutral-600 dark:bg-fv-neutral-800 dark:border-fv-neutral-600 dark:text-fv-neutral-200 dark:hover:bg-fv-neutral-800 dark:hover:text-fv-neutral-100"
150
+ href="javascript:void(0);"
151
+ :key="`${i}-sm`"
152
+ @click="page(items.page_no - (3 - i))"
153
+ >
154
+ {{ items.page_no - (3 - i) }}
155
+ </a>
156
+ </template>
157
+ <a
158
+ href="#"
159
+ aria-current="page"
160
+ class="z-10 relative inline-flex items-center px-4 pt-2 pb-1.5 border text-sm font-medium font-bold bg-fv-primary-50 border-fv-primary-500 text-fv-primary-600 hover:text-fv-neutral-600 dark:bg-fv-primary-900 dark:border-fv-primary-500 dark:text-fv-primary-200 dark:hover:text-fv-neutral-100"
161
+ >
162
+ {{ items.page_no }}
163
+ </a>
164
+ <template v-for="i in 2">
165
+ <a
166
+ v-if="items.page_no + i <= items.page_max"
167
+ class="relative inline-flex items-center px-4 pt-2 pb-1.5 border text-sm font-medium bg-fv-neutral-100 border-fv-neutral-300 text-fv-neutral-500 hover:bg-fv-neutral-200 hover:text-fv-neutral-600 dark:bg-fv-neutral-800 dark:border-fv-neutral-600 dark:text-fv-neutral-200 dark:hover:bg-fv-neutral-800 dark:hover:text-fv-neutral-100"
168
+ href="javascript:void(0);"
169
+ :key="`${i}-md`"
170
+ @click="page(items.page_no + i)"
171
+ >
172
+ {{ items.page_no + i }}
173
+ </a>
174
+ </template>
175
+ <span
176
+ v-if="items.page_no + 2 < items.page_max - 1"
177
+ class="relative inline-flex items-center px-2 pt-2 pb-1.5 border text-sm font-medium border-fv-neutral-300 bg-fv-neutral-100 text-fv-neutral-700 hover:text-fv-neutral-600 dark:border-fv-neutral-600 dark:bg-fv-neutral-800 dark:text-fv-neutral-700 dark:hover:text-fv-neutral-100"
178
+ >
179
+ ...
180
+ </span>
181
+ <a
182
+ v-if="items.page_no + 2 < items.page_max"
183
+ class="relative inline-flex items-center px-4 pt-2 pb-1.5 border text-sm font-medium bg-fv-neutral-100 border-fv-neutral-300 text-fv-neutral-500 hover:bg-fv-neutral-200 hover:text-fv-neutral-600 dark:bg-fv-neutral-800 dark:border-fv-neutral-600 dark:text-fv-neutral-200 dark:hover:bg-fv-neutral-800 dark:hover:text-fv-neutral-100"
184
+ href="javascript:void(0);"
185
+ @click="page(items.page_max)"
186
+ >
187
+ {{ items.page_max }}
188
+ </a>
189
+ <a
190
+ href="javascript:void(0);"
191
+ @click="next()"
192
+ v-if="items.page_no < items.page_max - 1"
193
+ class="relative inline-flex items-center px-2 pt-2 pb-1.5 border text-sm font-medium border-fv-neutral-300 bg-fv-neutral-100 text-fv-neutral-500 hover:bg-fv-neutral-200 hover:text-fv-neutral-600 dark:border-fv-neutral-600 dark:bg-fv-neutral-800 dark:text-fv-neutral-200 dark:hover:bg-fv-neutral-800 dark:hover:text-fv-neutral-100"
194
+ >
195
+ <span class="sr-only">{{ $t("next_paging") }}</span>
196
+ <ChevronRightIcon class="w-4 h-4" />
197
+ </a>
198
+ </nav>
199
+ <p
200
+ class="text-xs text-center italic mt-0.5 text-fv-neutral-700 dark:text-fv-neutral-300"
201
+ >
202
+ {{
203
+ $t("global_paging", {
204
+ start: items.results_per_page * (items.page_no - 1),
205
+ end: items.results_per_page * items.page_no,
206
+ total: items.count >= 10000 ? $t("paging_a_lot_of") : items.count,
207
+ })
208
+ }}
209
+ </p>
210
+ </div>
211
+ </div>
212
+ </template>
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <Transition name="collapse">
3
+ <slot></slot>
4
+ </Transition>
5
+ </template>
6
+
7
+ <style scoped>
8
+ .collapse-enter-active {
9
+ animation: collapse reverse 300ms ease;
10
+ }
11
+
12
+ .collapse-leave-active {
13
+ animation: collapse 300ms ease;
14
+ }
15
+
16
+ @keyframes collapse {
17
+ 100% {
18
+ max-height: 0px;
19
+ opacity: 0;
20
+ }
21
+
22
+ 50% {
23
+ max-height: 400px;
24
+ }
25
+
26
+ 0% {
27
+ opacity: 1;
28
+ }
29
+ }
30
+ </style>
@@ -0,0 +1,32 @@
1
+ <template>
2
+ <Transition name="expand">
3
+ <slot></slot>
4
+ </Transition>
5
+ </template>
6
+
7
+ <style scoped>
8
+ .expand-enter-active {
9
+ animation: expand reverse 300ms ease;
10
+ }
11
+
12
+ .expand-leave-active {
13
+ animation: expand 300ms ease;
14
+ }
15
+
16
+ @keyframes expand {
17
+ 100% {
18
+ max-height: 0px;
19
+ opacity: 0;
20
+ transform: scale(0.9);
21
+ }
22
+
23
+ 50% {
24
+ max-height: 400px;
25
+ }
26
+
27
+ 0% {
28
+ transform: scale(1);
29
+ opacity: 1;
30
+ }
31
+ }
32
+ </style>
@@ -0,0 +1,17 @@
1
+ <template>
2
+ <Transition name="fade" mode="out-in">
3
+ <slot></slot>
4
+ </Transition>
5
+ </template>
6
+
7
+ <style scoped>
8
+ .fade-enter-active,
9
+ .fade-leave-active {
10
+ transition: opacity 0.2s ease-in;
11
+ }
12
+
13
+ .fade-enter-from,
14
+ .fade-leave-to {
15
+ opacity: 0;
16
+ }
17
+ </style>