@fy-/fws-vue 2.2.62 → 2.2.64
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/fws/UserFlow.vue +192 -128
- package/components/ui/DefaultInput.vue +6 -5
- package/components/ui/DefaultNotif.vue +229 -41
- package/components/ui/DefaultPaging.vue +135 -29
- package/package.json +1 -1
|
@@ -219,161 +219,225 @@ onMounted(async () => {
|
|
|
219
219
|
<form
|
|
220
220
|
v-if="!completed"
|
|
221
221
|
class="fws-login w-full"
|
|
222
|
+
aria-labelledby="login-title"
|
|
222
223
|
@submit.prevent="userFlow()"
|
|
223
224
|
>
|
|
224
|
-
<!--
|
|
225
|
-
<div
|
|
225
|
+
<!-- Message / Header -->
|
|
226
|
+
<div
|
|
227
|
+
v-if="responseMessage"
|
|
228
|
+
class="fws-login__header mb-4"
|
|
229
|
+
>
|
|
226
230
|
<h2
|
|
227
|
-
|
|
228
|
-
class="text-lg text-fv-neutral-700 dark:text-fv-neutral-300
|
|
231
|
+
id="login-title"
|
|
232
|
+
class="text-lg font-medium text-fv-neutral-700 dark:text-fv-neutral-300"
|
|
229
233
|
>
|
|
230
234
|
{{ responseMessage }}
|
|
231
235
|
</h2>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
:key="`${field.label}oauth`"
|
|
257
|
-
class="h-12 w-12 block p-2 mr-3"
|
|
258
|
-
:alt="field.info.Name"
|
|
259
|
-
:src="field.button.logo"
|
|
260
|
-
>
|
|
261
|
-
<div>
|
|
262
|
-
{{
|
|
263
|
-
$t("user_flow_signin_with", {
|
|
264
|
-
provider: field.name,
|
|
265
|
-
})
|
|
266
|
-
}}
|
|
267
|
-
</div>
|
|
268
|
-
</a>
|
|
269
|
-
</template>
|
|
270
|
-
<button
|
|
271
|
-
type="button"
|
|
272
|
-
class="flex items-center gap-2 justify-start btn neutral defaults w-full mx-auto !font-semibold"
|
|
273
|
-
@click="
|
|
274
|
-
() => {
|
|
275
|
-
showEmail = true;
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<!-- OAuth providers section -->
|
|
239
|
+
<div v-if="hasOauth && !showEmail" class="fws-login__oauth space-y-3">
|
|
240
|
+
<div
|
|
241
|
+
v-for="field of responseFields"
|
|
242
|
+
v-show="field.type && field.type === 'oauth2' && field.button"
|
|
243
|
+
:key="field.id"
|
|
244
|
+
>
|
|
245
|
+
<button
|
|
246
|
+
type="button"
|
|
247
|
+
class="flex w-full items-center justify-start gap-3 px-4 py-2.5 rounded-lg border border-fv-neutral-200 dark:border-fv-neutral-700
|
|
248
|
+
transition-all duration-200 hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-fv-neutral-800
|
|
249
|
+
focus:ring-fv-primary-500 dark:focus:ring-fv-primary-600"
|
|
250
|
+
:style="`background: ${
|
|
251
|
+
field.button['background-color']
|
|
252
|
+
}; color: ${$getContrastingTextColor(
|
|
253
|
+
field.button['background-color'],
|
|
254
|
+
)}`"
|
|
255
|
+
:aria-label="`${$t('user_flow_signin_with', { provider: field.name })}`"
|
|
256
|
+
@click="
|
|
257
|
+
() => {
|
|
258
|
+
if (field.info.Button_Extra?.trigger) {
|
|
259
|
+
doTrigger(field);
|
|
276
260
|
}
|
|
277
|
-
|
|
261
|
+
else {
|
|
262
|
+
userFlow({ initial: true, oauth: field.id });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
"
|
|
266
|
+
>
|
|
267
|
+
<img
|
|
268
|
+
:key="`${field.label}oauth`"
|
|
269
|
+
class="h-6 w-6 flex-shrink-0"
|
|
270
|
+
:alt="field.info.Name"
|
|
271
|
+
:src="field.button.logo"
|
|
278
272
|
>
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
273
|
+
<span class="text-base font-medium">
|
|
274
|
+
{{ $t("user_flow_signin_with", { provider: field.name }) }}
|
|
275
|
+
</span>
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<div class="relative my-6">
|
|
280
|
+
<div class="absolute inset-0 flex items-center">
|
|
281
|
+
<div class="w-full border-t border-fv-neutral-200 dark:border-fv-neutral-700" />
|
|
288
282
|
</div>
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
283
|
+
<div class="relative flex justify-center text-sm">
|
|
284
|
+
<span class="px-2 bg-white dark:bg-fv-neutral-800 text-fv-neutral-500 dark:text-fv-neutral-400">
|
|
285
|
+
{{ $t("user_flow_or") }}
|
|
286
|
+
</span>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<button
|
|
291
|
+
type="button"
|
|
292
|
+
class="flex w-full items-center justify-start gap-3 px-4 py-2.5 rounded-lg border border-fv-neutral-200 dark:border-fv-neutral-700
|
|
293
|
+
bg-white dark:bg-fv-neutral-700 text-fv-neutral-800 dark:text-white
|
|
294
|
+
hover:bg-fv-neutral-50 dark:hover:bg-fv-neutral-650 transition-all duration-200
|
|
295
|
+
hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-fv-primary-500 dark:focus:ring-fv-primary-600"
|
|
296
|
+
aria-label="Sign in with email"
|
|
297
|
+
@click="showEmail = true"
|
|
293
298
|
>
|
|
294
|
-
<
|
|
295
|
-
|
|
299
|
+
<EnvelopeIcon class="h-6 w-6 text-fv-primary-500 dark:text-fv-primary-400 flex-shrink-0" />
|
|
300
|
+
<span class="text-base font-medium">
|
|
301
|
+
{{ $t("user_flow_signin_with", { provider: $t("user_flow_provider_email_cta") }) }}
|
|
302
|
+
</span>
|
|
303
|
+
</button>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<!-- Form fields section -->
|
|
307
|
+
<div
|
|
308
|
+
v-if="forceAction || (showEmail && initial) || !initial"
|
|
309
|
+
class="fws-login__form space-y-4"
|
|
310
|
+
>
|
|
311
|
+
<template v-if="responseFields && responseFields.length > 0">
|
|
312
|
+
<!-- Labels and text elements -->
|
|
313
|
+
<template v-for="field of responseFields" :key="field.label">
|
|
314
|
+
<div
|
|
315
|
+
v-if="field.type === 'label'"
|
|
316
|
+
class="mb-2"
|
|
317
|
+
>
|
|
296
318
|
<h3
|
|
297
|
-
|
|
298
|
-
class="
|
|
299
|
-
:class="
|
|
319
|
+
class="text-sm"
|
|
320
|
+
:class="[
|
|
300
321
|
field.style === 'error'
|
|
301
|
-
? '
|
|
302
|
-
: ''
|
|
303
|
-
"
|
|
322
|
+
? 'p-3 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 font-medium'
|
|
323
|
+
: 'text-fv-neutral-600 dark:text-fv-neutral-400',
|
|
324
|
+
]"
|
|
304
325
|
>
|
|
305
|
-
<a
|
|
306
|
-
field.
|
|
307
|
-
|
|
308
|
-
|
|
326
|
+
<a
|
|
327
|
+
v-if="field.link"
|
|
328
|
+
:href="field.link"
|
|
329
|
+
class="text-fv-primary-600 dark:text-fv-primary-400 hover:underline focus:outline-none focus:ring-2 focus:ring-fv-primary-500 dark:focus:ring-fv-primary-600 rounded"
|
|
330
|
+
>
|
|
331
|
+
{{ field.label }}
|
|
332
|
+
</a>
|
|
333
|
+
<span v-else v-html="field.label" />
|
|
309
334
|
</h3>
|
|
335
|
+
</div>
|
|
310
336
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
v-if="field.name"
|
|
325
|
-
:id="field.name"
|
|
326
|
-
ref="inputs"
|
|
327
|
-
v-model="formData[field.name]"
|
|
328
|
-
:label="field.label"
|
|
329
|
-
:mask="field.mask"
|
|
330
|
-
class="mt-3"
|
|
331
|
-
:placeholder="
|
|
332
|
-
field.name === 'name' ? 'John Doe' : field.label
|
|
333
|
-
"
|
|
334
|
-
:error="fieldsError[field.name]"
|
|
335
|
-
:type="field.type"
|
|
336
|
-
:req="responseReq.includes(field.name)"
|
|
337
|
-
:autocomplete="autocompleteValue(field.name)"
|
|
338
|
-
/>
|
|
339
|
-
</template>
|
|
340
|
-
</template>
|
|
341
|
-
<template v-if="field.type === 'checkbox'">
|
|
337
|
+
<!-- Input fields -->
|
|
338
|
+
<template v-if="field.cat === 'input'">
|
|
339
|
+
<template
|
|
340
|
+
v-if="
|
|
341
|
+
field.type === 'text'
|
|
342
|
+
|| field.type === 'password'
|
|
343
|
+
|| field.type === 'email'
|
|
344
|
+
|| field.type === 'mask'
|
|
345
|
+
|| field.type === 'tel'
|
|
346
|
+
|| field.type === 'number'
|
|
347
|
+
|| field.type === 'phone'
|
|
348
|
+
"
|
|
349
|
+
>
|
|
342
350
|
<DefaultInput
|
|
343
351
|
v-if="field.name"
|
|
344
352
|
:id="field.name"
|
|
345
|
-
|
|
346
|
-
|
|
353
|
+
ref="inputs"
|
|
354
|
+
v-model="formData[field.name]"
|
|
347
355
|
:label="field.label"
|
|
356
|
+
:mask="field.mask"
|
|
357
|
+
:placeholder="
|
|
358
|
+
field.name === 'name' ? 'John Doe' : field.label
|
|
359
|
+
"
|
|
348
360
|
:error="fieldsError[field.name]"
|
|
349
361
|
:type="field.type"
|
|
350
362
|
:req="responseReq.includes(field.name)"
|
|
351
|
-
:
|
|
363
|
+
:autocomplete="autocompleteValue(field.name)"
|
|
352
364
|
/>
|
|
353
365
|
</template>
|
|
354
366
|
</template>
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
>{{ $t("recover_pwd_link") }}</a>
|
|
370
|
-
</div>
|
|
371
|
-
<button class="btn primary medium mt-4">
|
|
372
|
-
{{ $t("cta_login_next") }}
|
|
373
|
-
</button>
|
|
367
|
+
|
|
368
|
+
<!-- Checkbox inputs -->
|
|
369
|
+
<template v-if="field.type === 'checkbox'">
|
|
370
|
+
<DefaultInput
|
|
371
|
+
v-if="field.name"
|
|
372
|
+
:id="field.name"
|
|
373
|
+
v-model:checkbox-value="formData[field.name]"
|
|
374
|
+
:label="field.label"
|
|
375
|
+
:error="fieldsError[field.name]"
|
|
376
|
+
:type="field.type"
|
|
377
|
+
:req="responseReq.includes(field.name)"
|
|
378
|
+
:link-icon="field.link"
|
|
379
|
+
/>
|
|
380
|
+
</template>
|
|
374
381
|
</template>
|
|
375
|
-
|
|
382
|
+
|
|
383
|
+
<!-- Error message -->
|
|
384
|
+
<div
|
|
385
|
+
v-if="responseError && responseError.token"
|
|
386
|
+
class="p-3 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 text-sm font-medium"
|
|
387
|
+
role="alert"
|
|
388
|
+
v-html="$t(responseError.token)"
|
|
389
|
+
/>
|
|
390
|
+
|
|
391
|
+
<!-- Password recovery link -->
|
|
392
|
+
<div
|
|
393
|
+
v-if="responseReq.includes('password') && 0"
|
|
394
|
+
class="text-right my-2"
|
|
395
|
+
>
|
|
396
|
+
<button
|
|
397
|
+
type="button"
|
|
398
|
+
class="text-fv-primary-600 dark:text-fv-primary-400 text-sm hover:underline focus:outline-none focus:ring-2 focus:ring-fv-primary-500 rounded"
|
|
399
|
+
@click="
|
|
400
|
+
() => {
|
|
401
|
+
eventBus.emit('ResetPasswordModal', true);
|
|
402
|
+
pwdRecoverMailSent = false;
|
|
403
|
+
}
|
|
404
|
+
"
|
|
405
|
+
>
|
|
406
|
+
{{ $t("recover_pwd_link") }}
|
|
407
|
+
</button>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<!-- Submit button -->
|
|
411
|
+
<button
|
|
412
|
+
type="submit"
|
|
413
|
+
class="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-lg shadow-sm
|
|
414
|
+
text-white bg-fv-primary-600 hover:bg-fv-primary-700 dark:bg-fv-primary-700 dark:hover:bg-fv-primary-800
|
|
415
|
+
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-fv-primary-500 dark:focus:ring-fv-primary-600
|
|
416
|
+
dark:focus:ring-offset-fv-neutral-800 font-medium transition-all duration-200"
|
|
417
|
+
aria-label="Continue"
|
|
418
|
+
>
|
|
419
|
+
{{ $t("cta_login_next") }}
|
|
420
|
+
</button>
|
|
421
|
+
</template>
|
|
376
422
|
</div>
|
|
377
423
|
</form>
|
|
378
424
|
</ClientOnly>
|
|
379
425
|
</template>
|
|
426
|
+
|
|
427
|
+
<style scoped>
|
|
428
|
+
.fws-login {
|
|
429
|
+
@apply transition-all duration-300;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.fws-login__oauth button,
|
|
433
|
+
.fws-login__form button[type="submit"] {
|
|
434
|
+
@apply transition-all duration-200;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
@media (max-width: 640px) {
|
|
438
|
+
.fws-login__oauth,
|
|
439
|
+
.fws-login__form {
|
|
440
|
+
@apply px-0;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
</style>
|
|
@@ -187,7 +187,7 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
187
187
|
:name="id"
|
|
188
188
|
:class="{
|
|
189
189
|
'error': checkErrors,
|
|
190
|
-
'bg-fv-neutral-50 border border-fv-neutral-300 text-fv-neutral-900 text-sm rounded-lg block w-full dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-
|
|
190
|
+
'bg-fv-neutral-50 border border-fv-neutral-300 text-fv-neutral-900 text-sm rounded-lg block w-full dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-300 dark:text-white transition-all duration-200': type !== 'range',
|
|
191
191
|
'p-2.5': type !== 'range',
|
|
192
192
|
'focus:border-fv-primary-500 dark:focus:border-fv-primary-500': !checkErrors,
|
|
193
193
|
'focus:ring-2 focus:ring-fv-primary-300 dark:focus:ring-fv-primary-800 focus:ring-opacity-50': type !== 'range',
|
|
@@ -288,7 +288,7 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
288
288
|
:aria-invalid="checkErrors ? 'true' : 'false'"
|
|
289
289
|
class="block p-2.5 w-full text-sm text-fv-neutral-900 bg-fv-neutral-50 rounded-lg
|
|
290
290
|
border border-fv-neutral-300 focus:ring-2 focus:ring-fv-primary-300 focus:border-fv-primary-500
|
|
291
|
-
dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-
|
|
291
|
+
dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-300
|
|
292
292
|
dark:text-white dark:focus:ring-fv-primary-800 dark:focus:border-fv-primary-500
|
|
293
293
|
transition-colors duration-200 shadow-sm"
|
|
294
294
|
@focus="handleFocus"
|
|
@@ -332,7 +332,7 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
332
332
|
:aria-invalid="checkErrors ? 'true' : 'false'"
|
|
333
333
|
class="block p-2.5 w-full text-sm text-fv-neutral-900 bg-fv-neutral-50
|
|
334
334
|
rounded-lg border border-fv-neutral-300 focus:ring-2 focus:ring-fv-primary-300 focus:border-fv-primary-500
|
|
335
|
-
dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-
|
|
335
|
+
dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-300
|
|
336
336
|
dark:text-white dark:focus:ring-fv-primary-800 dark:focus:border-fv-primary-500
|
|
337
337
|
transition-colors duration-200 shadow-sm min-h-[100px]"
|
|
338
338
|
@focus="handleFocus"
|
|
@@ -375,7 +375,7 @@ defineExpose({ focus, blur, getInputRef })
|
|
|
375
375
|
class="appearance-none bg-fv-neutral-50 border border-fv-neutral-300 text-fv-neutral-900 text-sm
|
|
376
376
|
rounded-lg focus:ring-2 focus:ring-fv-primary-300 focus:border-fv-primary-500
|
|
377
377
|
block w-full p-2.5 dark:bg-fv-neutral-700 dark:border-fv-neutral-600
|
|
378
|
-
dark:placeholder-fv-neutral-
|
|
378
|
+
dark:placeholder-fv-neutral-300 dark:text-white dark:focus:ring-fv-primary-800
|
|
379
379
|
dark:focus:border-fv-primary-500 shadow-sm transition-colors duration-200 pr-10"
|
|
380
380
|
@focus="handleFocus"
|
|
381
381
|
@blur="handleBlur"
|
|
@@ -577,6 +577,7 @@ input, select, textarea, input[type="range"]::-webkit-slider-thumb, input[type="
|
|
|
577
577
|
input::placeholder,
|
|
578
578
|
textarea::placeholder,
|
|
579
579
|
select::placeholder {
|
|
580
|
-
@apply text-fv-neutral-400 dark:text-fv-neutral-
|
|
580
|
+
@apply text-fv-neutral-400 dark:text-fv-neutral-300;
|
|
581
|
+
opacity: 0.8;
|
|
581
582
|
}
|
|
582
583
|
</style>
|
|
@@ -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>
|