@fy-/fws-vue 2.2.51 → 2.2.52

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.
@@ -2,19 +2,24 @@
2
2
  import type { Component } from 'vue'
3
3
  import type { APIPaging } from '../../composables/rest'
4
4
  import {
5
- ArrowLeftCircleIcon,
6
- ArrowRightCircleIcon,
7
5
  ChevronDoubleLeftIcon,
8
6
  ChevronDoubleRightIcon,
9
- XCircleIcon,
7
+ ChevronLeftIcon,
8
+ ChevronRightIcon,
9
+ InformationCircleIcon,
10
+ XMarkIcon,
10
11
  } from '@heroicons/vue/24/solid'
11
- import { computed, h, onMounted, onUnmounted, reactive, ref } from 'vue'
12
+ import { computed, h, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
12
13
  import { useEventBus } from '../../composables/event-bus'
13
14
  import DefaultPaging from './DefaultPaging.vue'
14
15
 
15
16
  const isGalleryOpen = ref<boolean>(false)
16
17
  const eventBus = useEventBus()
17
18
  const sidePanel = ref<boolean>(true)
19
+ const showControls = ref<boolean>(true)
20
+ const isFullscreen = ref<boolean>(false)
21
+ const infoPanel = ref<boolean>(false)
22
+ const touchStartTime = ref<number>(0)
18
23
 
19
24
  const props = withDefaults(
20
25
  defineProps<{
@@ -44,7 +49,7 @@ const props = withDefaults(
44
49
  imageComponent: 'img',
45
50
  mode: 'grid',
46
51
  gridHeight: 4,
47
- closeIcon: () => h(XCircleIcon),
52
+ closeIcon: () => h(XMarkIcon),
48
53
  images: () => [],
49
54
  isVideo: () => false,
50
55
  getImageUrl: (image: any) => image.image_url,
@@ -65,6 +70,8 @@ const modelValue = computed({
65
70
 
66
71
  const direction = ref<'next' | 'prev'>('next')
67
72
 
73
+ let controlsTimeout: number | null = null
74
+
68
75
  function setModal(value: boolean) {
69
76
  if (value === true) {
70
77
  if (props.onOpen) props.onOpen()
@@ -73,6 +80,12 @@ function setModal(value: boolean) {
73
80
  document.addEventListener('keydown', handleKeyboardInput)
74
81
  document.addEventListener('keyup', handleKeyboardRelease)
75
82
  }
83
+ // Auto-hide controls after 3 seconds on mobile
84
+ if (window.innerWidth < 1024) {
85
+ controlsTimeout = window.setTimeout(() => {
86
+ showControls.value = false
87
+ }, 3000)
88
+ }
76
89
  }
77
90
  else {
78
91
  if (props.onClose) props.onClose()
@@ -81,8 +94,20 @@ function setModal(value: boolean) {
81
94
  document.removeEventListener('keydown', handleKeyboardInput)
82
95
  document.removeEventListener('keyup', handleKeyboardRelease)
83
96
  }
97
+ // Clear timeout if modal is closed
98
+ if (controlsTimeout) {
99
+ clearTimeout(controlsTimeout)
100
+ controlsTimeout = null
101
+ }
102
+ // Exit fullscreen if active
103
+ if (isFullscreen.value && document.exitFullscreen) {
104
+ document.exitFullscreen().catch(() => {})
105
+ isFullscreen.value = false
106
+ }
84
107
  }
85
108
  isGalleryOpen.value = value
109
+ showControls.value = true
110
+ infoPanel.value = false
86
111
  }
87
112
 
88
113
  function openGalleryImage(index: number | undefined) {
@@ -103,6 +128,7 @@ function goNextImage() {
103
128
  else {
104
129
  modelValue.value = 0
105
130
  }
131
+ resetControlsTimer()
106
132
  }
107
133
 
108
134
  function goPrevImage() {
@@ -114,6 +140,7 @@ function goPrevImage() {
114
140
  modelValue.value
115
141
  = props.images.length - 1 > 0 ? props.images.length - 1 : 0
116
142
  }
143
+ resetControlsTimer()
117
144
  }
118
145
 
119
146
  const modelValueSrc = computed(() => {
@@ -122,12 +149,69 @@ const modelValueSrc = computed(() => {
122
149
  return props.getImageUrl(props.images[modelValue.value])
123
150
  })
124
151
 
152
+ const currentImage = computed(() => {
153
+ if (props.images.length === 0) return null
154
+ return props.images[modelValue.value]
155
+ })
156
+
157
+ const imageCount = computed(() => props.images.length)
158
+ const currentIndex = computed(() => modelValue.value + 1)
159
+
125
160
  const start = reactive({ x: 0, y: 0 })
126
161
 
162
+ function resetControlsTimer() {
163
+ // Show controls when user interacts
164
+ showControls.value = true
165
+
166
+ // Only set timer on mobile
167
+ if (window.innerWidth < 1024) {
168
+ if (controlsTimeout) {
169
+ clearTimeout(controlsTimeout)
170
+ }
171
+ controlsTimeout = window.setTimeout(() => {
172
+ showControls.value = false
173
+ }, 3000)
174
+ }
175
+ }
176
+
177
+ function toggleControls() {
178
+ showControls.value = !showControls.value
179
+ if (showControls.value && window.innerWidth < 1024) {
180
+ resetControlsTimer()
181
+ }
182
+ }
183
+
184
+ function toggleInfoPanel() {
185
+ infoPanel.value = !infoPanel.value
186
+ resetControlsTimer()
187
+ }
188
+
189
+ function toggleFullscreen() {
190
+ if (!isFullscreen.value) {
191
+ const element = document.querySelector('.gallery-container') as HTMLElement
192
+ if (element && element.requestFullscreen) {
193
+ element.requestFullscreen().then(() => {
194
+ isFullscreen.value = true
195
+ }).catch(() => {})
196
+ }
197
+ }
198
+ else {
199
+ if (document.exitFullscreen) {
200
+ document.exitFullscreen().then(() => {
201
+ isFullscreen.value = false
202
+ }).catch(() => {})
203
+ }
204
+ }
205
+ resetControlsTimer()
206
+ }
207
+
127
208
  function touchStart(event: TouchEvent) {
128
209
  const touch = event.touches[0]
129
210
  const targetElement = touch.target as HTMLElement
130
211
 
212
+ // Store start time for tap detection
213
+ touchStartTime.value = Date.now()
214
+
131
215
  // Check if the touch started on an interactive element
132
216
  if (targetElement.closest('button, a, input, textarea, select')) {
133
217
  return // Don't handle swipe if interacting with an interactive element
@@ -136,9 +220,11 @@ function touchStart(event: TouchEvent) {
136
220
  start.x = touch.screenX
137
221
  start.y = touch.screenY
138
222
  }
223
+
139
224
  function touchEnd(event: TouchEvent) {
140
225
  const touch = event.changedTouches[0]
141
226
  const targetElement = touch.target as HTMLElement
227
+ const touchDuration = Date.now() - touchStartTime.value
142
228
 
143
229
  // Check if the touch ended on an interactive element
144
230
  if (targetElement.closest('button, a, input, textarea, select')) {
@@ -150,6 +236,12 @@ function touchEnd(event: TouchEvent) {
150
236
  const diffX = start.x - end.x
151
237
  const diffY = start.y - end.y
152
238
 
239
+ // Detect tap (quick touch with minimal movement)
240
+ if (Math.abs(diffX) < 10 && Math.abs(diffY) < 10 && touchDuration < 300) {
241
+ toggleControls()
242
+ return
243
+ }
244
+
153
245
  // Add a threshold to prevent accidental swipes
154
246
  if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
155
247
  if (diffX > 0) {
@@ -191,6 +283,12 @@ function handleKeyboardInput(event: KeyboardEvent) {
191
283
  direction.value = 'prev'
192
284
  goPrevImage()
193
285
  break
286
+ case 'f':
287
+ toggleFullscreen()
288
+ break
289
+ case 'i':
290
+ toggleInfoPanel()
291
+ break
194
292
  default:
195
293
  break
196
294
  }
@@ -213,6 +311,11 @@ function handleBackdropClick(event: MouseEvent) {
213
311
  }
214
312
  }
215
313
 
314
+ watch(currentImage, () => {
315
+ // Reset info panel when image changes
316
+ infoPanel.value = false
317
+ })
318
+
216
319
  onMounted(() => {
217
320
  eventBus.on(`${props.id}GalleryImage`, openGalleryImage)
218
321
  eventBus.on(`${props.id}Gallery`, openGalleryImage)
@@ -228,6 +331,10 @@ onUnmounted(() => {
228
331
  document.removeEventListener('keyup', handleKeyboardRelease)
229
332
  document.body.style.overflow = '' // Ensure body scrolling is restored
230
333
  }
334
+ // Clear any remaining timeouts
335
+ if (controlsTimeout) {
336
+ clearTimeout(controlsTimeout)
337
+ }
231
338
  })
232
339
  </script>
233
340
 
@@ -243,43 +350,50 @@ onUnmounted(() => {
243
350
  >
244
351
  <div
245
352
  v-if="isGalleryOpen"
246
- class="fixed bg-fv-neutral-900 text-white inset-0 max-w-[100vw] overflow-y-auto overflow-x-hidden"
353
+ class="fixed bg-fv-neutral-900 text-white inset-0 max-w-[100vw] overflow-hidden gallery-container"
247
354
  style="z-index: 37"
248
355
  role="dialog"
249
356
  aria-modal="true"
250
357
  @click="handleBackdropClick"
251
358
  >
252
359
  <div
253
- class="relative w-full max-w-full flex flex-col justify-center items-center"
360
+ class="relative w-full h-full max-w-full flex flex-col justify-center items-center"
254
361
  style="z-index: 38"
255
362
  @click.stop
256
363
  >
257
- <div class="flex flex-grow gap-4 w-full max-w-full">
258
- <div class="flex-grow h-[100vh] flex items-center relative">
259
- <button
260
- class="btn w-9 h-9 rounded-full absolute top-4 left-2"
261
- style="z-index: 39"
262
- aria-label="Close gallery"
263
- @click="setModal(false)"
264
- >
265
- <component :is="closeIcon" class="w-8 h-8" />
266
- </button>
267
-
364
+ <!-- Main Content Area -->
365
+ <div class="flex flex-grow gap-4 w-full h-full max-w-full">
366
+ <div class="flex-grow h-full flex items-center relative">
367
+ <!-- Image Display Area -->
268
368
  <div
269
- class="flex h-[100vh] relative flex-grow items-center justify-center gap-2 z-[1]"
369
+ class="flex h-full relative flex-grow items-center justify-center gap-2 z-[1]"
370
+ @touchstart="touchStart"
371
+ @touchend="touchEnd"
270
372
  >
271
- <div
272
- class="hidden lg:relative z-[2] lg:flex w-10 flex-shrink-0 items-center justify-center flex-0"
373
+ <!-- Image Navigation - Left -->
374
+ <transition
375
+ enter-active-class="transition-opacity duration-300"
376
+ enter-from-class="opacity-0"
377
+ enter-to-class="opacity-100"
378
+ leave-active-class="transition-opacity duration-300"
379
+ leave-from-class="opacity-100"
380
+ leave-to-class="opacity-0"
273
381
  >
274
- <button
275
- v-if="images.length > 1"
276
- class="btn p-1 rounded-full"
277
- aria-label="Previous image"
278
- @click="goPrevImage()"
382
+ <div
383
+ v-if="showControls && images.length > 1"
384
+ class="absolute left-0 z-[40] h-full flex items-center px-2 md:px-4"
279
385
  >
280
- <ArrowLeftCircleIcon class="w-8 h-8" />
281
- </button>
282
- </div>
386
+ <button
387
+ class="btn bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 backdrop-blur-sm p-2 rounded-full transition-transform transform hover:scale-110"
388
+ aria-label="Previous image"
389
+ @click="goPrevImage()"
390
+ >
391
+ <ChevronLeftIcon class="w-6 h-6 md:w-8 md:h-8" />
392
+ </button>
393
+ </div>
394
+ </transition>
395
+
396
+ <!-- Main Image Container -->
283
397
  <div
284
398
  class="flex-1 flex flex-col z-[2] items-center justify-center max-w-full lg:max-w-[calc(100vw - 256px)] relative"
285
399
  >
@@ -288,14 +402,11 @@ onUnmounted(() => {
288
402
  mode="out-in"
289
403
  >
290
404
  <div
291
- v-if="true"
292
405
  :key="`image-display-${modelValue}`"
293
406
  class="flex-1 w-full max-w-full flex flex-col items-center justify-center absolute inset-0 z-[2]"
294
407
  >
295
408
  <div
296
409
  class="flex-1 w-full max-w-full flex items-center justify-center"
297
- @touchstart="touchStart"
298
- @touchend="touchEnd"
299
410
  >
300
411
  <template
301
412
  v-if="videoComponent && isVideo(images[modelValue])"
@@ -325,42 +436,44 @@ onUnmounted(() => {
325
436
  />
326
437
  </template>
327
438
  </div>
439
+
440
+ <!-- Image Slot Content -->
328
441
  <div
329
- class="flex-0 py-2 flex items-center justify-center max-w-full w-full relative !z-[3]"
442
+ v-if="infoPanel"
443
+ class="flex-0 px-4 py-3 backdrop-blur-md bg-fv-neutral-900/70 rounded-t-lg flex items-center justify-center max-w-full w-full !z-[45] transition-all"
330
444
  >
331
445
  <slot :value="images[modelValue]" />
332
446
  </div>
333
447
  </div>
334
448
  </transition>
335
449
  </div>
336
- <div
337
- class="hidden lg:flex w-10 flex-shrink-0 items-center justify-center"
450
+
451
+ <!-- Image Navigation - Right -->
452
+ <transition
453
+ enter-active-class="transition-opacity duration-300"
454
+ enter-from-class="opacity-0"
455
+ enter-to-class="opacity-100"
456
+ leave-active-class="transition-opacity duration-300"
457
+ leave-from-class="opacity-100"
458
+ leave-to-class="opacity-0"
338
459
  >
339
- <button
340
- class="btn w-9 h-9 rounded-full hidden lg:block absolute top-4"
341
- :class="{
342
- '-right-4': sidePanel,
343
- 'right-2': !sidePanel,
344
- }"
345
- style="z-index: 39"
346
- :aria-label="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
347
- @click="() => (sidePanel = !sidePanel)"
348
- >
349
- <ChevronDoubleRightIcon v-if="sidePanel" class="w-7 h-7" />
350
- <ChevronDoubleLeftIcon v-else class="w-7 h-7" />
351
- </button>
352
- <button
353
- v-if="images.length > 1"
354
- class="btn p-1 rounded-full"
355
- aria-label="Next image"
356
- @click="goNextImage()"
460
+ <div
461
+ v-if="showControls && images.length > 1"
462
+ class="absolute right-0 z-[40] h-full flex items-center px-2 md:px-4"
357
463
  >
358
- <ArrowRightCircleIcon class="w-8 h-8" />
359
- </button>
360
- </div>
464
+ <button
465
+ class="btn bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 backdrop-blur-sm p-2 rounded-full transition-transform transform hover:scale-110"
466
+ aria-label="Next image"
467
+ @click="goNextImage()"
468
+ >
469
+ <ChevronRightIcon class="w-6 h-6 md:w-8 md:h-8" />
470
+ </button>
471
+ </div>
472
+ </transition>
361
473
  </div>
362
474
  </div>
363
475
 
476
+ <!-- Side Panel for Thumbnails -->
364
477
  <transition
365
478
  enter-active-class="transform transition ease-in-out duration-300"
366
479
  enter-from-class="translate-x-full"
@@ -371,27 +484,33 @@ onUnmounted(() => {
371
484
  >
372
485
  <div
373
486
  v-if="sidePanel"
374
- class="hidden lg:block flex-shrink-0 w-64 bg-fv-neutral-800 h-[100vh] max-h-[100vh] overflow-y-auto"
487
+ class="hidden lg:block flex-shrink-0 w-64 bg-fv-neutral-800/90 backdrop-blur-md h-full max-h-full overflow-y-auto"
375
488
  >
376
- <!-- Side panel content -->
377
- <div v-if="paging" class="flex items-center justify-center">
489
+ <!-- Paging Controls -->
490
+ <div v-if="paging" class="flex items-center justify-center pt-2">
378
491
  <DefaultPaging :id="id" :items="paging" />
379
492
  </div>
493
+
494
+ <!-- Thumbnail Grid -->
380
495
  <div class="grid grid-cols-2 gap-2 p-2">
381
496
  <div
382
497
  v-for="i in images.length"
383
498
  :key="`bg_${id}_${i}`"
384
- class="hover:!brightness-100"
385
- :style="{
386
- filter:
387
- i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.5)',
388
- }"
499
+ class="group relative"
389
500
  >
501
+ <div
502
+ class="absolute inset-0 rounded-lg transition-colors duration-300 group-hover:bg-fv-neutral-700/40"
503
+ :class="{ 'bg-fv-primary-500/40': i - 1 === modelValue }"
504
+ />
390
505
  <img
391
506
  v-if="imageComponent === 'img'"
392
- :class="`h-auto max-w-full rounded-lg cursor-pointer shadow ${getBorderColor(
507
+ :class="`h-auto max-w-full rounded-lg cursor-pointer shadow transition-all duration-300 group-hover:brightness-110 ${getBorderColor(
393
508
  images[i - 1],
394
509
  )}`"
510
+ :style="{
511
+ filter:
512
+ i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.7)',
513
+ }"
395
514
  :src="getThumbnailUrl(images[i - 1])"
396
515
  :alt="`Thumbnail ${i}`"
397
516
  @click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
@@ -402,9 +521,17 @@ onUnmounted(() => {
402
521
  :image="getThumbnailUrl(images[i - 1]).image"
403
522
  :variant="getThumbnailUrl(images[i - 1]).variant"
404
523
  :alt="getThumbnailUrl(images[i - 1]).alt"
405
- :class="`h-auto max-w-full rounded-lg cursor-pointer shadow ${getBorderColor(
524
+ :class="`h-auto max-w-full rounded-lg cursor-pointer shadow transition-all duration-300 group-hover:brightness-110 ${getBorderColor(
406
525
  images[i - 1],
407
526
  )}`"
527
+ :style="{
528
+ filter:
529
+ i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.7)',
530
+ }"
531
+ :likes="getThumbnailUrl(images[i - 1]).likes"
532
+ :show-likes="getThumbnailUrl(images[i - 1]).showLikes"
533
+ :is-author="getThumbnailUrl(images[i - 1]).isAuthor"
534
+ :user-uuid="getThumbnailUrl(images[i - 1]).userUUID"
408
535
  @click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
409
536
  />
410
537
  </div>
@@ -412,18 +539,121 @@ onUnmounted(() => {
412
539
  </div>
413
540
  </transition>
414
541
  </div>
542
+
543
+ <!-- Top Controls -->
544
+ <transition
545
+ enter-active-class="transition-opacity duration-300"
546
+ enter-from-class="opacity-0"
547
+ enter-to-class="opacity-100"
548
+ leave-active-class="transition-opacity duration-300"
549
+ leave-from-class="opacity-100"
550
+ leave-to-class="opacity-0"
551
+ >
552
+ <div
553
+ v-if="showControls"
554
+ class="absolute top-0 left-0 right-0 px-4 py-3 flex justify-between items-center bg-gradient-to-b from-fv-neutral-900/90 to-transparent backdrop-blur-sm z-[50] transition-opacity"
555
+ >
556
+ <!-- Title and Counter -->
557
+ <div class="flex items-center space-x-2">
558
+ <span v-if="title" class="font-medium text-lg">{{ title }}</span>
559
+ <span class="text-sm opacity-80">{{ currentIndex }} / {{ imageCount }}</span>
560
+ </div>
561
+
562
+ <!-- Control Buttons -->
563
+ <div class="flex items-center space-x-2">
564
+ <button
565
+ class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
566
+ title="Toggle info"
567
+ @click="toggleInfoPanel"
568
+ >
569
+ <InformationCircleIcon class="w-5 h-5" />
570
+ </button>
571
+
572
+ <button
573
+ class="btn p-1.5 rounded-full lg:hidden bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
574
+ :title="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
575
+ @click="() => (sidePanel = !sidePanel)"
576
+ >
577
+ <ChevronDoubleRightIcon v-if="sidePanel" class="w-5 h-5" />
578
+ <ChevronDoubleLeftIcon v-else class="w-5 h-5" />
579
+ </button>
580
+
581
+ <button
582
+ class="btn p-1.5 rounded-full hidden lg:block bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
583
+ :title="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
584
+ @click="() => (sidePanel = !sidePanel)"
585
+ >
586
+ <ChevronDoubleRightIcon v-if="sidePanel" class="w-5 h-5" />
587
+ <ChevronDoubleLeftIcon v-else class="w-5 h-5" />
588
+ </button>
589
+
590
+ <button
591
+ class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
592
+ aria-label="Close gallery"
593
+ @click="setModal(false)"
594
+ >
595
+ <component :is="closeIcon" class="w-5 h-5" />
596
+ </button>
597
+ </div>
598
+ </div>
599
+ </transition>
600
+
601
+ <!-- Mobile Thumbnail Preview -->
602
+ <transition
603
+ enter-active-class="transition-transform duration-300 ease-out"
604
+ enter-from-class="translate-y-full"
605
+ enter-to-class="translate-y-0"
606
+ leave-active-class="transition-transform duration-300 ease-in"
607
+ leave-from-class="translate-y-0"
608
+ leave-to-class="translate-y-full"
609
+ >
610
+ <div
611
+ v-if="showControls && images.length > 1 && !sidePanel"
612
+ class="absolute bottom-0 left-0 right-0 p-2 lg:hidden bg-gradient-to-t from-fv-neutral-900/90 to-transparent backdrop-blur-sm z-[50]"
613
+ >
614
+ <div class="overflow-x-auto flex space-x-2 pb-1 px-1">
615
+ <div
616
+ v-for="(image, idx) in images"
617
+ :key="`mobile_thumb_${id}_${idx}`"
618
+ class="flex-shrink-0 w-16 h-16 rounded-lg relative cursor-pointer"
619
+ :class="{ 'ring-2 ring-fv-primary-500 ring-offset-1 ring-offset-fv-neutral-900': idx === modelValue }"
620
+ @click="$eventBus.emit(`${id}GalleryImage`, idx)"
621
+ >
622
+ <img
623
+ v-if="imageComponent === 'img'"
624
+ class="w-full h-full object-cover rounded-lg transition duration-200"
625
+ :style="{
626
+ filter: idx === modelValue ? 'brightness(1)' : 'brightness(0.7)',
627
+ }"
628
+ :src="getThumbnailUrl(image)"
629
+ :alt="`Thumbnail ${idx + 1}`"
630
+ >
631
+ <component
632
+ :is="imageComponent"
633
+ v-else
634
+ :image="getThumbnailUrl(image).image"
635
+ :variant="getThumbnailUrl(image).variant"
636
+ :alt="getThumbnailUrl(image).alt"
637
+ class="w-full h-full object-cover rounded-lg transition duration-200"
638
+ :style="{
639
+ filter: idx === modelValue ? 'brightness(1)' : 'brightness(0.7)',
640
+ }"
641
+ />
642
+ </div>
643
+ </div>
644
+ </div>
645
+ </transition>
415
646
  </div>
416
647
  </div>
417
648
  </transition>
418
649
 
419
- <div v-if="mode === 'grid' || mode === 'mason' || mode === 'custom'" class="min-h-[600px]">
650
+ <!-- Thumbnail Grid/Mason/Custom Layouts -->
651
+ <div v-if="mode === 'grid' || mode === 'mason' || mode === 'custom'" class="gallery-grid">
420
652
  <div
421
653
  :class="{
422
- ' grid grid-cols-2 md:grid-cols-4 xl:grid-cols-6 gap-4 items-start':
423
- mode === 'mason',
424
- ' grid grid-cols-2 md:grid-cols-4 xl:grid-cols-6 gap-4 items-center':
425
- mode === 'grid',
426
- ' custom-grid': mode === 'custom',
654
+ 'masonry-grid': mode === 'mason',
655
+ 'standard-grid': mode === 'grid',
656
+ 'custom-grid': mode === 'custom',
427
657
  }"
428
658
  >
429
659
  <slot name="thumbnail" />
@@ -431,17 +661,17 @@ onUnmounted(() => {
431
661
  <template v-if="mode === 'mason'">
432
662
  <div
433
663
  v-if="i + (1 % gridHeight) === 0"
434
- class="grid gap-4 items-start relative"
664
+ class="masonry-column relative"
435
665
  >
436
666
  <div v-if="ranking" class="img-gallery-ranking">
437
667
  {{ i }}
438
668
  </div>
439
669
 
440
670
  <template v-for="j in gridHeight" :key="`gi_${id}_${i + j}`">
441
- <div>
671
+ <div class="masonry-item">
442
672
  <img
443
673
  v-if="i + j - 2 < images.length && imageComponent === 'img'"
444
- class="h-auto max-w-full rounded-lg cursor-pointer"
674
+ class="h-auto max-w-full w-full rounded-lg cursor-pointer shadow-md hover:shadow-xl transition-all duration-300 hover:brightness-110 hover:scale-[1.02]"
445
675
  :src="getThumbnailUrl(images[i + j - 2])"
446
676
  :alt="`Gallery image ${i + j - 1}`"
447
677
  @click="$eventBus.emit(`${id}GalleryImage`, i + j - 2)"
@@ -452,7 +682,7 @@ onUnmounted(() => {
452
682
  :image="getThumbnailUrl(images[i + j - 2]).image"
453
683
  :variant="getThumbnailUrl(images[i + j - 2]).variant"
454
684
  :alt="getThumbnailUrl(images[i + j - 2]).alt"
455
- :class="`h-auto max-w-full rounded-lg cursor-pointer ${getBorderColor(
685
+ :class="`h-auto max-w-full w-full rounded-lg cursor-pointer shadow-md hover:shadow-xl transition-all duration-300 hover:brightness-110 hover:scale-[1.02] ${getBorderColor(
456
686
  images[i + j - 2],
457
687
  )}`"
458
688
  :likes="getThumbnailUrl(images[i + j - 2]).likes"
@@ -465,42 +695,47 @@ onUnmounted(() => {
465
695
  </template>
466
696
  </div>
467
697
  </template>
468
- <div v-else class="relative">
698
+ <div v-else class="grid-item relative group">
469
699
  <div v-if="ranking" class="img-gallery-ranking">
470
700
  {{ i }}
471
701
  </div>
472
- <img
473
- v-if="imageComponent === 'img'"
474
- class="h-auto max-w-full rounded-lg cursor-pointer"
475
- :src="getThumbnailUrl(images[i - 1])"
476
- :alt="`Gallery image ${i}`"
477
- @click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
478
- >
479
- <component
480
- :is="imageComponent"
481
- v-else-if="imageComponent"
482
- :image="getThumbnailUrl(images[i - 1]).image"
483
- :variant="getThumbnailUrl(images[i - 1]).variant"
484
- :alt="getThumbnailUrl(images[i - 1]).alt"
485
- :class="`h-auto max-w-full rounded-lg cursor-pointer ${getBorderColor(
486
- images[i - 1],
487
- )}`"
488
- :likes="getThumbnailUrl(images[i - 1]).likes"
489
- :show-likes="getThumbnailUrl(images[i - 1]).showLikes"
490
- :is-author="getThumbnailUrl(images[i - 1]).isAuthor"
491
- :user-uuid="getThumbnailUrl(images[i - 1]).userUUID"
492
- @click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
493
- />
702
+ <div class="overflow-hidden rounded-lg">
703
+ <img
704
+ v-if="imageComponent === 'img'"
705
+ class="h-auto max-w-full w-full rounded-lg cursor-pointer shadow-md transition-all duration-300 group-hover:brightness-110 group-hover:scale-[1.03]"
706
+ :src="getThumbnailUrl(images[i - 1])"
707
+ :alt="`Gallery image ${i}`"
708
+ @click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
709
+ >
710
+ <component
711
+ :is="imageComponent"
712
+ v-else-if="imageComponent"
713
+ :image="getThumbnailUrl(images[i - 1]).image"
714
+ :variant="getThumbnailUrl(images[i - 1]).variant"
715
+ :alt="getThumbnailUrl(images[i - 1]).alt"
716
+ :class="`h-auto max-w-full w-full rounded-lg cursor-pointer shadow-md transition-all duration-300 group-hover:brightness-110 group-hover:scale-[1.03] ${getBorderColor(
717
+ images[i - 1],
718
+ )}`"
719
+ :likes="getThumbnailUrl(images[i - 1]).likes"
720
+ :show-likes="getThumbnailUrl(images[i - 1]).showLikes"
721
+ :is-author="getThumbnailUrl(images[i - 1]).isAuthor"
722
+ :user-uuid="getThumbnailUrl(images[i - 1]).userUUID"
723
+ @click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
724
+ />
725
+ </div>
494
726
  </div>
495
727
  </template>
496
728
  </div>
497
729
  </div>
730
+
731
+ <!-- Button Mode -->
498
732
  <button
499
733
  v-if="mode === 'button'"
500
- :class="`btn ${buttonType ? buttonType : 'primary'} defaults`"
734
+ :class="`btn ${buttonType ? buttonType : 'primary'} defaults relative overflow-hidden group`"
501
735
  @click="openGalleryImage(0)"
502
736
  >
503
- {{ buttonText ? buttonText : $t("open_gallery_cta") }}
737
+ <span class="relative z-10">{{ buttonText ? buttonText : $t("open_gallery_cta") }}</span>
738
+ <span class="absolute inset-0 bg-white/10 transform -translate-x-full group-hover:translate-x-0 transition-transform duration-300" />
504
739
  </button>
505
740
  </div>
506
741
  </template>
@@ -572,18 +807,101 @@ onUnmounted(() => {
572
807
  filter: blur(10px);
573
808
  }
574
809
 
575
- /* Ensure the images are positioned correctly to prevent overlap */
576
- .relative-container {
577
- position: relative;
578
- width: 100%;
579
- height: 100%;
810
+ /* Modern grids */
811
+ .gallery-grid {
812
+ min-height: 200px;
813
+ }
814
+
815
+ .standard-grid {
816
+ display: grid;
817
+ grid-template-columns: repeat(1, 1fr);
818
+ gap: 0.75rem;
819
+ }
820
+
821
+ @media (min-width: 480px) {
822
+ .standard-grid {
823
+ grid-template-columns: repeat(2, 1fr);
824
+ }
825
+ }
826
+
827
+ @media (min-width: 768px) {
828
+ .standard-grid {
829
+ grid-template-columns: repeat(3, 1fr);
830
+ gap: 1rem;
831
+ }
832
+ }
833
+
834
+ @media (min-width: 1024px) {
835
+ .standard-grid {
836
+ grid-template-columns: repeat(4, 1fr);
837
+ }
838
+ }
839
+
840
+ @media (min-width: 1280px) {
841
+ .standard-grid {
842
+ grid-template-columns: repeat(5, 1fr);
843
+ }
844
+ }
845
+
846
+ @media (min-width: 1536px) {
847
+ .standard-grid {
848
+ grid-template-columns: repeat(6, 1fr);
849
+ }
850
+ }
851
+
852
+ .masonry-grid {
853
+ display: grid;
854
+ grid-template-columns: repeat(1, 1fr);
855
+ gap: 0.75rem;
856
+ }
857
+
858
+ @media (min-width: 480px) {
859
+ .masonry-grid {
860
+ grid-template-columns: repeat(2, 1fr);
861
+ }
862
+ }
863
+
864
+ @media (min-width: 768px) {
865
+ .masonry-grid {
866
+ grid-template-columns: repeat(3, 1fr);
867
+ gap: 1rem;
868
+ }
869
+ }
870
+
871
+ @media (min-width: 1024px) {
872
+ .masonry-grid {
873
+ grid-template-columns: repeat(4, 1fr);
874
+ }
875
+ }
876
+
877
+ .masonry-column {
878
+ display: grid;
879
+ gap: 0.75rem;
880
+ }
881
+
882
+ .masonry-item {
883
+ break-inside: avoid;
884
+ margin-bottom: 0.75rem;
885
+ }
886
+
887
+ .grid-item {
888
+ break-inside: avoid;
889
+ margin-bottom: 0.75rem;
580
890
  }
581
891
 
582
- .relative-container > div {
892
+ .img-gallery-ranking {
583
893
  position: absolute;
584
- top: 0;
585
- left: 0;
586
- width: 100%;
587
- height: 100%;
894
+ top: 0.5rem;
895
+ left: 0.5rem;
896
+ background-color: rgba(0, 0, 0, 0.6);
897
+ color: white;
898
+ width: 1.5rem;
899
+ height: 1.5rem;
900
+ display: flex;
901
+ align-items: center;
902
+ justify-content: center;
903
+ border-radius: 9999px;
904
+ font-size: 0.75rem;
905
+ z-index: 10;
588
906
  }
589
907
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fy-/fws-vue",
3
- "version": "2.2.51",
3
+ "version": "2.2.52",
4
4
  "author": "Florian 'Fy' Gasquez <m@fy.to>",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/fy-to/FWJS#readme",