@fy-/fws-vue 2.2.62 → 2.2.63
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.
- package/components/ui/DefaultNotif.vue +229 -41
- package/components/ui/DefaultPaging.vue +135 -29
- package/package.json +1 -1
|
@@ -3,10 +3,11 @@ import type { Component } from 'vue'
|
|
|
3
3
|
import {
|
|
4
4
|
CheckCircleIcon,
|
|
5
5
|
ExclamationTriangleIcon,
|
|
6
|
-
|
|
6
|
+
InformationCircleIcon,
|
|
7
7
|
SparklesIcon,
|
|
8
|
+
XMarkIcon,
|
|
8
9
|
} from '@heroicons/vue/24/solid'
|
|
9
|
-
import { onMounted, onUnmounted, ref } from 'vue'
|
|
10
|
+
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
10
11
|
import { useEventBus } from '../../composables/event-bus'
|
|
11
12
|
import ScaleTransition from './transitions/ScaleTransition.vue'
|
|
12
13
|
|
|
@@ -31,9 +32,12 @@ const eventBus = useEventBus()
|
|
|
31
32
|
/** Current displayed notification */
|
|
32
33
|
const currentNotif = ref<NotifProps | null>(null)
|
|
33
34
|
|
|
34
|
-
/** Progress percentage (0 to 100) for the notification
|
|
35
|
+
/** Progress percentage (0 to 100) for the notification's life */
|
|
35
36
|
const progress = ref(0)
|
|
36
37
|
|
|
38
|
+
/** Is the notification paused (e.g., when hovering) */
|
|
39
|
+
const isPaused = ref(false)
|
|
40
|
+
|
|
37
41
|
/** References to setTimeout / setInterval so we can clear them properly */
|
|
38
42
|
let hideTimeout: ReturnType<typeof setTimeout> | null = null
|
|
39
43
|
let progressInterval: ReturnType<typeof setInterval> | null = null
|
|
@@ -55,7 +59,7 @@ function onCall(data: NotifProps) {
|
|
|
55
59
|
|
|
56
60
|
// Automatically compute an icon if none is provided
|
|
57
61
|
if (!data.imgIcon) {
|
|
58
|
-
if (data.type === 'info') data.imgIcon =
|
|
62
|
+
if (data.type === 'info') data.imgIcon = InformationCircleIcon
|
|
59
63
|
else if (data.type === 'warning') data.imgIcon = ExclamationTriangleIcon
|
|
60
64
|
else if (data.type === 'success') data.imgIcon = CheckCircleIcon
|
|
61
65
|
else if (data.type === 'secret') data.imgIcon = SparklesIcon
|
|
@@ -72,7 +76,7 @@ function onCall(data: NotifProps) {
|
|
|
72
76
|
// (B) Animate the progress bar from 0 to 100% within that time
|
|
73
77
|
progress.value = 0
|
|
74
78
|
progressInterval = setInterval(() => {
|
|
75
|
-
if (currentNotif.value && data.time) {
|
|
79
|
+
if (currentNotif.value && data.time && !isPaused.value) {
|
|
76
80
|
// update progress based on a 100ms tick
|
|
77
81
|
progress.value += (100 / (data.time / 100))
|
|
78
82
|
// if progress hits or exceeds 100, hide
|
|
@@ -89,6 +93,7 @@ function onCall(data: NotifProps) {
|
|
|
89
93
|
function hideNotif() {
|
|
90
94
|
currentNotif.value = null
|
|
91
95
|
progress.value = 0
|
|
96
|
+
isPaused.value = false
|
|
92
97
|
|
|
93
98
|
if (hideTimeout) {
|
|
94
99
|
clearTimeout(hideTimeout)
|
|
@@ -100,6 +105,124 @@ function hideNotif() {
|
|
|
100
105
|
}
|
|
101
106
|
}
|
|
102
107
|
|
|
108
|
+
/** Pause the notification timer on hover */
|
|
109
|
+
function pauseTimer() {
|
|
110
|
+
isPaused.value = true
|
|
111
|
+
|
|
112
|
+
if (hideTimeout) {
|
|
113
|
+
clearTimeout(hideTimeout)
|
|
114
|
+
hideTimeout = null
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Resume the notification timer after hover */
|
|
119
|
+
function resumeTimer() {
|
|
120
|
+
if (!currentNotif.value) return
|
|
121
|
+
|
|
122
|
+
isPaused.value = false
|
|
123
|
+
|
|
124
|
+
// Calculate remaining time based on progress
|
|
125
|
+
const remainingTime = currentNotif.value.time
|
|
126
|
+
? Math.max(currentNotif.value.time * (1 - progress.value / 100), 1000)
|
|
127
|
+
: 5000
|
|
128
|
+
|
|
129
|
+
// Reset the timeout with the remaining time
|
|
130
|
+
hideTimeout = setTimeout(() => hideNotif(), remainingTime)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Execute CTA action if provided */
|
|
134
|
+
function handleCtaClick() {
|
|
135
|
+
if (currentNotif.value?.ctaAction) {
|
|
136
|
+
currentNotif.value.ctaAction()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Get ARIA label based on notification type */
|
|
141
|
+
const ariaDescribedBy = computed(() => {
|
|
142
|
+
if (!currentNotif.value) return ''
|
|
143
|
+
return `notif-${currentNotif.value.type || 'info'}`
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
/** Get background color based on notification type */
|
|
147
|
+
const bgColor = computed(() => {
|
|
148
|
+
if (!currentNotif.value) return ''
|
|
149
|
+
|
|
150
|
+
switch (currentNotif.value.type) {
|
|
151
|
+
case 'success':
|
|
152
|
+
return 'bg-green-50 dark:bg-green-900/20'
|
|
153
|
+
case 'warning':
|
|
154
|
+
return 'bg-amber-50 dark:bg-amber-900/20'
|
|
155
|
+
case 'secret':
|
|
156
|
+
return 'bg-fuchsia-50 dark:bg-fuchsia-900/20'
|
|
157
|
+
default: // info
|
|
158
|
+
return 'bg-blue-50 dark:bg-blue-900/20'
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
/** Get border color based on notification type */
|
|
163
|
+
const borderColor = computed(() => {
|
|
164
|
+
if (!currentNotif.value) return ''
|
|
165
|
+
|
|
166
|
+
switch (currentNotif.value.type) {
|
|
167
|
+
case 'success':
|
|
168
|
+
return 'border-green-300 dark:border-green-700'
|
|
169
|
+
case 'warning':
|
|
170
|
+
return 'border-amber-300 dark:border-amber-700'
|
|
171
|
+
case 'secret':
|
|
172
|
+
return 'border-fuchsia-300 dark:border-fuchsia-700'
|
|
173
|
+
default: // info
|
|
174
|
+
return 'border-blue-300 dark:border-blue-700'
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
/** Get text color based on notification type */
|
|
179
|
+
const textColor = computed(() => {
|
|
180
|
+
if (!currentNotif.value) return ''
|
|
181
|
+
|
|
182
|
+
switch (currentNotif.value.type) {
|
|
183
|
+
case 'success':
|
|
184
|
+
return 'text-green-800 dark:text-green-200'
|
|
185
|
+
case 'warning':
|
|
186
|
+
return 'text-amber-800 dark:text-amber-200'
|
|
187
|
+
case 'secret':
|
|
188
|
+
return 'text-fuchsia-800 dark:text-fuchsia-200'
|
|
189
|
+
default: // info
|
|
190
|
+
return 'text-blue-800 dark:text-blue-200'
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
/** Get icon color based on notification type */
|
|
195
|
+
const iconColor = computed(() => {
|
|
196
|
+
if (!currentNotif.value) return ''
|
|
197
|
+
|
|
198
|
+
switch (currentNotif.value.type) {
|
|
199
|
+
case 'success':
|
|
200
|
+
return 'text-green-500 dark:text-green-400'
|
|
201
|
+
case 'warning':
|
|
202
|
+
return 'text-amber-500 dark:text-amber-400'
|
|
203
|
+
case 'secret':
|
|
204
|
+
return 'text-fuchsia-500 dark:text-fuchsia-400'
|
|
205
|
+
default: // info
|
|
206
|
+
return 'text-blue-500 dark:text-blue-400'
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
/** Get progress bar color based on notification type */
|
|
211
|
+
const progressColor = computed(() => {
|
|
212
|
+
if (!currentNotif.value) return ''
|
|
213
|
+
|
|
214
|
+
switch (currentNotif.value.type) {
|
|
215
|
+
case 'success':
|
|
216
|
+
return 'bg-green-500 dark:bg-green-400'
|
|
217
|
+
case 'warning':
|
|
218
|
+
return 'bg-amber-500 dark:bg-amber-400'
|
|
219
|
+
case 'secret':
|
|
220
|
+
return 'bg-fuchsia-500 dark:bg-fuchsia-400'
|
|
221
|
+
default: // info
|
|
222
|
+
return 'bg-blue-500 dark:bg-blue-400'
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
|
|
103
226
|
/**
|
|
104
227
|
* Setup: Listen to the global event bus
|
|
105
228
|
*/
|
|
@@ -120,62 +243,127 @@ onUnmounted(() => {
|
|
|
120
243
|
<div
|
|
121
244
|
v-if="currentNotif !== null"
|
|
122
245
|
id="base-notif"
|
|
123
|
-
class="
|
|
246
|
+
class="fixed bottom-4 right-4 sm:right-8 z-[2000] max-w-sm w-full sm:w-96 rounded-lg border shadow-lg overflow-hidden backdrop-blur-sm transition-all duration-300 transform"
|
|
124
247
|
role="alert"
|
|
125
|
-
:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
currentNotif.type === 'warning',
|
|
130
|
-
'text-green-800 border-green-300 dark:text-green-300 dark:border-green-800':
|
|
131
|
-
currentNotif.type === 'success',
|
|
132
|
-
'text-fuchsia-800 border-fuchsia-300 dark:text-fuchsia-300 dark:border-fuchsia-800':
|
|
133
|
-
currentNotif.type === 'secret',
|
|
134
|
-
}"
|
|
248
|
+
:aria-describedby="ariaDescribedBy"
|
|
249
|
+
:class="[bgColor, borderColor, textColor]"
|
|
250
|
+
@mouseenter="pauseTimer"
|
|
251
|
+
@mouseleave="resumeTimer"
|
|
135
252
|
>
|
|
136
|
-
|
|
137
|
-
|
|
253
|
+
<!-- Progress bar -->
|
|
254
|
+
<div class="relative h-1 bg-gray-200 dark:bg-gray-700 overflow-hidden">
|
|
138
255
|
<div
|
|
139
|
-
class="absolute left-0 top-0 h-full
|
|
256
|
+
class="absolute left-0 top-0 h-full transition-[width] ease-linear"
|
|
257
|
+
:class="progressColor"
|
|
140
258
|
:style="{ width: `${progress}%` }"
|
|
141
259
|
/>
|
|
142
260
|
</div>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
:
|
|
149
|
-
|
|
261
|
+
|
|
262
|
+
<div class="p-4">
|
|
263
|
+
<!-- Header with icon and title -->
|
|
264
|
+
<div class="flex items-center justify-between">
|
|
265
|
+
<div class="flex items-center gap-3">
|
|
266
|
+
<div class="flex-shrink-0" :class="[iconColor]">
|
|
267
|
+
<img
|
|
268
|
+
v-if="currentNotif.imgSrc"
|
|
269
|
+
class="w-6 h-6 rounded-full"
|
|
270
|
+
:src="currentNotif.imgSrc"
|
|
271
|
+
:alt="currentNotif.title"
|
|
272
|
+
>
|
|
273
|
+
<component
|
|
274
|
+
:is="currentNotif.imgIcon"
|
|
275
|
+
v-else
|
|
276
|
+
class="w-6 h-6"
|
|
277
|
+
aria-hidden="true"
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
<h3
|
|
281
|
+
:id="ariaDescribedBy"
|
|
282
|
+
class="text-base font-semibold truncate"
|
|
283
|
+
v-text="currentNotif.title"
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<!-- Close button -->
|
|
288
|
+
<button
|
|
289
|
+
type="button"
|
|
290
|
+
class="inline-flex rounded-md p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-50 dark:focus:ring-offset-gray-900"
|
|
291
|
+
:class="iconColor"
|
|
292
|
+
aria-label="Close notification"
|
|
293
|
+
@click="hideNotif"
|
|
150
294
|
>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
v-else
|
|
154
|
-
class="flex-shrink-0 w-6 h-6"
|
|
155
|
-
/>
|
|
156
|
-
<h3 class="text-lg font-medium" v-text="currentNotif.title" />
|
|
295
|
+
<XMarkIcon class="h-5 w-5" aria-hidden="true" />
|
|
296
|
+
</button>
|
|
157
297
|
</div>
|
|
158
298
|
|
|
159
|
-
<!--
|
|
299
|
+
<!-- Notification content -->
|
|
160
300
|
<div
|
|
161
301
|
v-if="currentNotif.content"
|
|
162
|
-
class="mt-2 text-sm
|
|
302
|
+
class="mt-2 text-sm"
|
|
303
|
+
:class="textColor"
|
|
163
304
|
v-html="currentNotif.content"
|
|
164
305
|
/>
|
|
165
306
|
|
|
166
|
-
<!-- CTA
|
|
167
|
-
<div
|
|
307
|
+
<!-- CTA buttons -->
|
|
308
|
+
<div
|
|
309
|
+
v-if="currentNotif.ctaText || currentNotif.ctaLink || currentNotif.ctaAction"
|
|
310
|
+
class="mt-3 flex justify-end gap-2"
|
|
311
|
+
>
|
|
312
|
+
<a
|
|
313
|
+
v-if="currentNotif.ctaLink"
|
|
314
|
+
:href="currentNotif.ctaLink"
|
|
315
|
+
class="inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2"
|
|
316
|
+
:class="{
|
|
317
|
+
'bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500': currentNotif.type === 'info' || !currentNotif.type,
|
|
318
|
+
'bg-amber-600 hover:bg-amber-700 text-white focus:ring-amber-500': currentNotif.type === 'warning',
|
|
319
|
+
'bg-green-600 hover:bg-green-700 text-white focus:ring-green-500': currentNotif.type === 'success',
|
|
320
|
+
'bg-fuchsia-600 hover:bg-fuchsia-700 text-white focus:ring-fuchsia-500': currentNotif.type === 'secret',
|
|
321
|
+
}"
|
|
322
|
+
>
|
|
323
|
+
{{ currentNotif.ctaText || $t("action_cta") }}
|
|
324
|
+
</a>
|
|
168
325
|
<button
|
|
326
|
+
v-else-if="currentNotif.ctaAction"
|
|
169
327
|
type="button"
|
|
170
|
-
class="
|
|
171
|
-
|
|
172
|
-
|
|
328
|
+
class="inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2"
|
|
329
|
+
:class="{
|
|
330
|
+
'bg-blue-600 hover:bg-blue-700 text-white focus:ring-blue-500': currentNotif.type === 'info' || !currentNotif.type,
|
|
331
|
+
'bg-amber-600 hover:bg-amber-700 text-white focus:ring-amber-500': currentNotif.type === 'warning',
|
|
332
|
+
'bg-green-600 hover:bg-green-700 text-white focus:ring-green-500': currentNotif.type === 'success',
|
|
333
|
+
'bg-fuchsia-600 hover:bg-fuchsia-700 text-white focus:ring-fuchsia-500': currentNotif.type === 'secret',
|
|
334
|
+
}"
|
|
335
|
+
@click="handleCtaClick"
|
|
173
336
|
>
|
|
174
|
-
|
|
175
|
-
{{ $t("dismiss_cta") }}
|
|
337
|
+
{{ currentNotif.ctaText || $t("action_cta") }}
|
|
176
338
|
</button>
|
|
177
339
|
</div>
|
|
178
340
|
</div>
|
|
179
341
|
</div>
|
|
180
342
|
</ScaleTransition>
|
|
181
343
|
</template>
|
|
344
|
+
|
|
345
|
+
<style scoped>
|
|
346
|
+
/* Optional: Add animation for notifications */
|
|
347
|
+
@keyframes slide-in-right {
|
|
348
|
+
0% {
|
|
349
|
+
transform: translateX(100%);
|
|
350
|
+
opacity: 0;
|
|
351
|
+
}
|
|
352
|
+
100% {
|
|
353
|
+
transform: translateX(0);
|
|
354
|
+
opacity: 1;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
#base-notif {
|
|
359
|
+
animation: slide-in-right 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
|
|
360
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* Improve dark mode */
|
|
364
|
+
@media (prefers-color-scheme: dark) {
|
|
365
|
+
#base-notif {
|
|
366
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2), 0 2px 4px -1px rgba(0, 0, 0, 0.1);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
</style>
|
|
@@ -153,100 +153,144 @@ onMounted(() => {
|
|
|
153
153
|
<template>
|
|
154
154
|
<div
|
|
155
155
|
v-if="items && items.page_max > 1 && items.page_no"
|
|
156
|
-
class="flex items-center justify-center"
|
|
156
|
+
class="flex flex-col items-center justify-center"
|
|
157
157
|
>
|
|
158
|
-
<div class="paging-container">
|
|
159
|
-
<nav aria-label="Pagination">
|
|
160
|
-
<ul class="
|
|
161
|
-
|
|
158
|
+
<div class="paging-container w-full">
|
|
159
|
+
<nav aria-label="Pagination" class="mb-2 flex justify-center">
|
|
160
|
+
<ul class="pagination-list">
|
|
161
|
+
<!-- Previous Button -->
|
|
162
|
+
<li v-if="items.page_no >= 2" class="pagination-item md:block">
|
|
162
163
|
<button
|
|
163
164
|
type="button"
|
|
164
|
-
class="
|
|
165
|
+
class="pagination-button pagination-nav-button"
|
|
166
|
+
:aria-label="$t('previous_paging')"
|
|
167
|
+
title="Previous page"
|
|
165
168
|
@click="prev()"
|
|
166
169
|
>
|
|
170
|
+
<ChevronLeftIcon class="w-5 h-5" aria-hidden="true" />
|
|
167
171
|
<span class="sr-only">{{ $t("previous_paging") }}</span>
|
|
168
|
-
<ChevronLeftIcon class="w-4 h-4" />
|
|
169
172
|
</button>
|
|
170
173
|
</li>
|
|
171
|
-
<li v-
|
|
174
|
+
<li v-else class="pagination-item invisible md:hidden">
|
|
175
|
+
<div class="pagination-placeholder">
|
|
176
|
+
<ChevronLeftIcon class="w-5 h-5 invisible" aria-hidden="true" />
|
|
177
|
+
</div>
|
|
178
|
+
</li>
|
|
179
|
+
|
|
180
|
+
<!-- First Page -->
|
|
181
|
+
<li v-if="items.page_no - 2 > 1" class="pagination-item hidden md:block">
|
|
172
182
|
<router-link
|
|
173
|
-
class="
|
|
183
|
+
class="pagination-link"
|
|
174
184
|
:to="page(1)"
|
|
185
|
+
aria-label="Go to page 1"
|
|
175
186
|
>
|
|
176
187
|
1
|
|
177
188
|
</router-link>
|
|
178
189
|
</li>
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
>
|
|
183
|
-
|
|
190
|
+
|
|
191
|
+
<!-- Ellipsis after first page -->
|
|
192
|
+
<li v-if="items.page_no - 2 > 2" class="pagination-item hidden md:block" aria-hidden="true">
|
|
193
|
+
<div class="pagination-ellipsis">
|
|
194
|
+
<span>•••</span>
|
|
184
195
|
</div>
|
|
185
196
|
</li>
|
|
197
|
+
|
|
198
|
+
<!-- Pages before current page -->
|
|
186
199
|
<template v-for="i in 2">
|
|
187
200
|
<li
|
|
188
201
|
v-if="items.page_no - (3 - i) >= 1"
|
|
189
202
|
:key="`page-${items.page_no - (3 - i)}`"
|
|
203
|
+
class="pagination-item hidden sm:block"
|
|
190
204
|
>
|
|
191
205
|
<router-link
|
|
192
|
-
class="
|
|
206
|
+
class="pagination-link"
|
|
193
207
|
:to="page(items.page_no - (3 - i))"
|
|
208
|
+
:aria-label="`Go to page ${items.page_no - (3 - i)}`"
|
|
194
209
|
>
|
|
195
210
|
{{ items.page_no - (3 - i) }}
|
|
196
211
|
</router-link>
|
|
197
212
|
</li>
|
|
198
213
|
</template>
|
|
199
|
-
|
|
214
|
+
|
|
215
|
+
<!-- Current Page -->
|
|
216
|
+
<li class="pagination-item">
|
|
200
217
|
<div
|
|
201
218
|
aria-current="page"
|
|
202
|
-
class="
|
|
219
|
+
class="pagination-current"
|
|
220
|
+
:aria-label="`Current page, Page ${items.page_no}`"
|
|
203
221
|
>
|
|
204
222
|
{{ items.page_no }}
|
|
205
223
|
</div>
|
|
206
224
|
</li>
|
|
225
|
+
|
|
226
|
+
<!-- Pages after current page -->
|
|
207
227
|
<template v-for="i in 2">
|
|
208
228
|
<li
|
|
209
229
|
v-if="items.page_no + i <= items.page_max"
|
|
210
230
|
:key="`page-x-${items.page_no + i}`"
|
|
231
|
+
class="pagination-item hidden sm:block"
|
|
211
232
|
>
|
|
212
233
|
<router-link
|
|
213
|
-
class="
|
|
234
|
+
class="pagination-link"
|
|
214
235
|
:to="page(items.page_no + i)"
|
|
236
|
+
:aria-label="`Go to page ${items.page_no + i}`"
|
|
215
237
|
>
|
|
216
238
|
{{ items.page_no + i }}
|
|
217
239
|
</router-link>
|
|
218
240
|
</li>
|
|
219
241
|
</template>
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
>
|
|
224
|
-
|
|
242
|
+
|
|
243
|
+
<!-- Ellipsis before last page -->
|
|
244
|
+
<li v-if="items.page_no + 2 < items.page_max - 1" class="pagination-item hidden md:block" aria-hidden="true">
|
|
245
|
+
<div class="pagination-ellipsis">
|
|
246
|
+
<span>•••</span>
|
|
225
247
|
</div>
|
|
226
248
|
</li>
|
|
227
|
-
|
|
249
|
+
|
|
250
|
+
<!-- Last Page -->
|
|
251
|
+
<li v-if="items.page_no + 2 < items.page_max" class="pagination-item hidden md:block">
|
|
228
252
|
<router-link
|
|
229
|
-
class="
|
|
253
|
+
class="pagination-link"
|
|
230
254
|
:to="page(items.page_max)"
|
|
255
|
+
:aria-label="`Go to page ${items.page_max}`"
|
|
231
256
|
>
|
|
232
257
|
{{ items.page_max }}
|
|
233
258
|
</router-link>
|
|
234
259
|
</li>
|
|
235
|
-
|
|
260
|
+
|
|
261
|
+
<!-- Next Button -->
|
|
262
|
+
<li v-if="items.page_no < items.page_max" class="pagination-item md:block">
|
|
236
263
|
<button
|
|
237
264
|
type="button"
|
|
238
|
-
class="
|
|
265
|
+
class="pagination-button pagination-nav-button"
|
|
266
|
+
:aria-label="$t('next_paging')"
|
|
267
|
+
title="Next page"
|
|
239
268
|
@click="next()"
|
|
240
269
|
>
|
|
270
|
+
<ChevronRightIcon class="w-5 h-5" aria-hidden="true" />
|
|
241
271
|
<span class="sr-only">{{ $t("next_paging") }}</span>
|
|
242
|
-
<ChevronRightIcon class="w-4 h-4" />
|
|
243
272
|
</button>
|
|
244
273
|
</li>
|
|
274
|
+
<li v-else class="pagination-item invisible md:hidden">
|
|
275
|
+
<div class="pagination-placeholder">
|
|
276
|
+
<ChevronRightIcon class="w-5 h-5 invisible" aria-hidden="true" />
|
|
277
|
+
</div>
|
|
278
|
+
</li>
|
|
245
279
|
</ul>
|
|
246
280
|
</nav>
|
|
281
|
+
|
|
282
|
+
<!-- Mobile page indication (x of y) -->
|
|
283
|
+
<div class="sm:hidden text-center mb-2">
|
|
284
|
+
<span class="text-sm font-medium text-fv-neutral-700 dark:text-fv-neutral-200">
|
|
285
|
+
Page {{ items.page_no }} of {{ items.page_max }}
|
|
286
|
+
</span>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<!-- Results summary -->
|
|
247
290
|
<p
|
|
248
291
|
v-if="showLegend"
|
|
249
|
-
class="text-xs text-fv-neutral-700 dark:text-fv-neutral-400
|
|
292
|
+
class="text-xs text-center text-fv-neutral-700 dark:text-fv-neutral-400"
|
|
293
|
+
aria-live="polite"
|
|
250
294
|
>
|
|
251
295
|
{{
|
|
252
296
|
$t("global_paging", {
|
|
@@ -259,3 +303,65 @@ onMounted(() => {
|
|
|
259
303
|
</div>
|
|
260
304
|
</div>
|
|
261
305
|
</template>
|
|
306
|
+
|
|
307
|
+
<style scoped>
|
|
308
|
+
.pagination-list {
|
|
309
|
+
@apply inline-flex items-center justify-center gap-1 shadow-sm rounded-lg;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.pagination-item {
|
|
313
|
+
@apply flex items-center justify-center;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.pagination-link,
|
|
317
|
+
.pagination-button,
|
|
318
|
+
.pagination-current,
|
|
319
|
+
.pagination-ellipsis,
|
|
320
|
+
.pagination-placeholder {
|
|
321
|
+
@apply flex items-center justify-center;
|
|
322
|
+
min-width: 2.25rem;
|
|
323
|
+
height: 2.25rem;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.pagination-link {
|
|
327
|
+
@apply px-3 py-2 rounded-md text-sm font-medium bg-white border border-fv-neutral-200
|
|
328
|
+
text-fv-neutral-700 hover:bg-fv-neutral-50 hover:text-fv-primary-600
|
|
329
|
+
focus:z-10 focus:outline-none focus:ring-2 focus:ring-fv-primary-500 focus:ring-offset-1
|
|
330
|
+
transition-colors duration-200
|
|
331
|
+
dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-200
|
|
332
|
+
dark:hover:bg-fv-neutral-700 dark:hover:text-white
|
|
333
|
+
dark:focus:ring-fv-primary-500;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.pagination-current {
|
|
337
|
+
@apply px-3 py-2 rounded-md text-sm font-bold
|
|
338
|
+
bg-fv-primary-100 text-fv-primary-700 border border-fv-primary-300
|
|
339
|
+
dark:bg-fv-primary-900 dark:text-fv-primary-100 dark:border-fv-primary-700;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.pagination-nav-button {
|
|
343
|
+
@apply p-2 rounded-md text-fv-neutral-600 bg-white border border-fv-neutral-200
|
|
344
|
+
hover:bg-fv-neutral-50 hover:text-fv-primary-600
|
|
345
|
+
focus:z-10 focus:outline-none focus:ring-2 focus:ring-fv-primary-500 focus:ring-offset-1
|
|
346
|
+
transition-colors duration-200
|
|
347
|
+
dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-300
|
|
348
|
+
dark:hover:bg-fv-neutral-700 dark:hover:text-white;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.pagination-ellipsis {
|
|
352
|
+
@apply px-2 py-1 text-fv-neutral-500 dark:text-fv-neutral-400;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
@media (max-width: 640px) {
|
|
356
|
+
.pagination-list {
|
|
357
|
+
@apply gap-2;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.pagination-link,
|
|
361
|
+
.pagination-button,
|
|
362
|
+
.pagination-current {
|
|
363
|
+
min-width: 2rem;
|
|
364
|
+
height: 2rem;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
</style>
|