@fy-/fws-vue 2.2.61 → 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.
@@ -3,10 +3,11 @@ import type { Component } from 'vue'
3
3
  import {
4
4
  CheckCircleIcon,
5
5
  ExclamationTriangleIcon,
6
- LightBulbIcon,
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 notifications life */
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 = LightBulbIcon
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=" mb-4 fixed bottom-4 right-8 !z-[2000] bg-fv-neutral-50/[.6] dark:bg-neutral-800/[.85] rounded-lg border overflow-hidden shadow-lg"
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
- :class="{
126
- 'text-fv-neutral-800 border-fv-neutral-300 dark:text-fv-neutral-400 dark:border-fv-neutral-600':
127
- currentNotif.type === 'info',
128
- 'text-red-800 border-red-300 dark:text-red-300 dark:border-red-800':
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
- <div class="relative h-[4px] bg-fv-neutral-900/[.2] rounded-full overflow-hidden ">
137
- <!-- We re-use text color (text-*) as background or define a custom color -->
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 bg-current transition-[width]"
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
- <div class="p-2">
144
- <div class="flex items-center gap-2">
145
- <img
146
- v-if="currentNotif.imgSrc"
147
- class="flex-shrink-0 w-6 h-6"
148
- :src="currentNotif.imgSrc"
149
- :alt="currentNotif.title"
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
- <component
152
- :is="currentNotif.imgIcon"
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
- <!-- Optional content -->
299
+ <!-- Notification content -->
160
300
  <div
161
301
  v-if="currentNotif.content"
162
- class="mt-2 text-sm prose-sm prose-invert"
302
+ class="mt-2 text-sm"
303
+ :class="textColor"
163
304
  v-html="currentNotif.content"
164
305
  />
165
306
 
166
- <!-- CTA row (if you need more buttons, just extend it) -->
167
- <div class="flex justify-end gap-2 pt-3">
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="btn neutral small"
171
- aria-label="Close"
172
- @click="hideNotif"
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
- <!-- i18n example, or plain text like "Dismiss" -->
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="flex items-center -space-x-px h-8 text-sm">
161
- <li v-if="items.page_no >= 2">
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="flex items-center justify-center px-1.5 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 hover:bg-fv-neutral-100 hover:text-fv-neutral-700 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400 dark:hover:bg-fv-neutral-700 dark:hover:text-white"
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-if="items.page_no - 2 > 1">
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="flex items-center justify-center px-3 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 hover:bg-fv-neutral-100 hover:text-fv-neutral-700 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400 dark:hover:bg-fv-neutral-700 dark:hover:text-white"
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
- <li v-if="items.page_no - 2 > 2">
180
- <div
181
- class="flex items-center justify-center px-1.5 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400"
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="flex items-center justify-center px-3 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 hover:bg-fv-neutral-100 hover:text-fv-neutral-700 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400 dark:hover:bg-fv-neutral-700 dark:hover:text-white"
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
- <li>
214
+
215
+ <!-- Current Page -->
216
+ <li class="pagination-item">
200
217
  <div
201
218
  aria-current="page"
202
- class="z-10 flex items-center justify-center px-3 h-8 leading-tight text-primary-600 border border-primary-300 bg-primary-50 dark:border-fv-neutral-700 dark:bg-fv-neutral-700 dark:text-white"
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="flex items-center justify-center px-3 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 hover:bg-fv-neutral-100 hover:text-fv-neutral-700 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400 dark:hover:bg-fv-neutral-700 dark:hover:text-white"
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
- <li v-if="items.page_no + 2 < items.page_max - 1">
221
- <div
222
- class="flex items-center justify-center px-1.5 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400"
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
- <li v-if="items.page_no + 2 < items.page_max">
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="flex items-center justify-center px-3 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 hover:bg-fv-neutral-100 hover:text-fv-neutral-700 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400 dark:hover:bg-fv-neutral-700 dark:hover:text-white"
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
- <li v-if="items.page_no < items.page_max">
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="flex items-center justify-center px-1.5 h-8 leading-tight text-fv-neutral-500 bg-white border border-fv-neutral-300 hover:bg-fv-neutral-100 hover:text-fv-neutral-700 dark:bg-fv-neutral-800 dark:border-fv-neutral-700 dark:text-fv-neutral-400 dark:hover:bg-fv-neutral-700 dark:hover:text-white"
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 pt-0.5"
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>
@@ -257,25 +257,6 @@ function handleKeyNavigation(e: KeyboardEvent, index: number) {
257
257
 
258
258
  <template>
259
259
  <div class="space-y-2 w-full">
260
- <!-- Optional label with help text -->
261
- <div v-if="label" class="flex items-center flex-wrap gap-1">
262
- <label
263
- :id="`label_tags_${id}`"
264
- :for="`tags_${id}`"
265
- class="block text-sm font-medium dark:text-white"
266
- >
267
- {{ label }}
268
- </label>
269
- <!-- Optional help text -->
270
- <span
271
- v-if="help"
272
- :id="`help_tags_${id}`"
273
- class="text-xs text-fv-neutral-500 dark:text-fv-neutral-300"
274
- >
275
- {{ help }}
276
- </span>
277
- </div>
278
-
279
260
  <div
280
261
  ref="inputContainer"
281
262
  class="tags-input"
@@ -346,12 +327,19 @@ function handleKeyNavigation(e: KeyboardEvent, index: number) {
346
327
  @paste.prevent="handlePaste"
347
328
  />
348
329
  </div>
349
-
350
- <!-- Tag counter when maxTags is set -->
330
+ <div v-if="label" class="flex items-center flex-wrap gap-1">
331
+ <span
332
+ v-if="help"
333
+ :id="`help_tags_${id}`"
334
+ class="text-xs text-fv-neutral-500 dark:text-fv-neutral-300"
335
+ >
336
+ {{ help }}
337
+ </span>
338
+ <!-- Tag counter when maxTags is set -->
339
+ </div>
351
340
  <div v-if="maxTags > 0" class="tag-counter">
352
341
  <span>{{ model.length }}/{{ maxTags }}</span>
353
342
  </div>
354
-
355
343
  <!-- Inline error display if needed -->
356
344
  <p
357
345
  v-if="$props.error"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fy-/fws-vue",
3
- "version": "2.2.61",
3
+ "version": "2.2.63",
4
4
  "author": "Florian 'Fy' Gasquez <m@fy.to>",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/fy-to/FWJS#readme",