@fy-/fws-vue 2.2.48 → 2.2.49

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.
@@ -4,7 +4,6 @@ import type { APIPaging } from '../../composables/rest'
4
4
  import {
5
5
  ArrowLeftCircleIcon,
6
6
  ArrowRightCircleIcon,
7
- ArrowsPointingOutIcon, // For fullscreen button
8
7
  ChevronDoubleLeftIcon,
9
8
  ChevronDoubleRightIcon,
10
9
  XCircleIcon,
@@ -16,20 +15,6 @@ import DefaultPaging from './DefaultPaging.vue'
16
15
  const isGalleryOpen = ref<boolean>(false)
17
16
  const eventBus = useEventBus()
18
17
  const sidePanel = ref<boolean>(true)
19
- const isImageLoading = ref<boolean>(false)
20
- const galleryRef = ref<HTMLElement | null>(null)
21
- const isFullscreen = ref<boolean>(false)
22
-
23
- // For mobile image manipulation
24
- const scale = ref<number>(1)
25
- const startDistance = ref<number>(0)
26
- const isPinching = ref<boolean>(false)
27
- const panPosition = reactive({ x: 0, y: 0 })
28
- const startPanPosition = reactive({ x: 0, y: 0 })
29
- const isPanning = ref<boolean>(false)
30
- const lastTapTime = ref<number>(0)
31
- const touchCenter = reactive({ x: 0.5, y: 0.5 }) // Default to center
32
- const showMobileThumbnails = ref<boolean>(false)
33
18
 
34
19
  const props = withDefaults(
35
20
  defineProps<{
@@ -79,47 +64,6 @@ const modelValue = computed({
79
64
  })
80
65
 
81
66
  const direction = ref<'next' | 'prev'>('next')
82
- const imageCounter = computed(() => `${modelValue.value + 1} / ${props.images.length}`)
83
- let focusableElements: HTMLElement[] = []
84
-
85
- // Trap focus within gallery for accessibility
86
- function getFocusableElements(element: HTMLElement): HTMLElement[] {
87
- return Array.from(
88
- element.querySelectorAll(
89
- 'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])',
90
- ),
91
- ).filter(
92
- el => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden'),
93
- ) as HTMLElement[]
94
- }
95
-
96
- function handleTabKey(event: KeyboardEvent) {
97
- if (!isGalleryOpen.value || event.key !== 'Tab' || focusableElements.length <= 1) return
98
-
99
- const firstElement = focusableElements[0]
100
- const lastElement = focusableElements[focusableElements.length - 1]
101
-
102
- // Shift + Tab on first element should focus the last element
103
- if (event.shiftKey && document.activeElement === firstElement) {
104
- event.preventDefault()
105
- lastElement.focus()
106
- }
107
- // Tab on last element should focus the first element
108
- else if (!event.shiftKey && document.activeElement === lastElement) {
109
- event.preventDefault()
110
- firstElement.focus()
111
- }
112
- }
113
-
114
- // Toggle fullscreen mode
115
- function toggleFullscreen() {
116
- isFullscreen.value = !isFullscreen.value
117
-
118
- // Reset zoom and pan when toggling fullscreen
119
- scale.value = 1
120
- panPosition.x = 0
121
- panPosition.y = 0
122
- }
123
67
 
124
68
  function setModal(value: boolean) {
125
69
  if (value === true) {
@@ -128,39 +72,14 @@ function setModal(value: boolean) {
128
72
  if (!import.meta.env.SSR) {
129
73
  document.addEventListener('keydown', handleKeyboardInput)
130
74
  document.addEventListener('keyup', handleKeyboardRelease)
131
- document.addEventListener('keydown', handleTabKey)
132
75
  }
133
-
134
- // Initialize focus trap after gallery opens
135
- setTimeout(() => {
136
- if (galleryRef.value) {
137
- focusableElements = getFocusableElements(galleryRef.value)
138
- // Focus the close button if present
139
- const closeButton = galleryRef.value.querySelector('button[aria-label="Close gallery"]') as HTMLElement
140
- if (closeButton) {
141
- closeButton.focus()
142
- }
143
- else if (focusableElements.length > 0) {
144
- focusableElements[0].focus()
145
- }
146
- }
147
- }, 100)
148
76
  }
149
77
  else {
150
78
  if (props.onClose) props.onClose()
151
79
  document.body.style.overflow = '' // Restore scrolling
152
-
153
- // Reset zoom, pan, fullscreen and thumbnail states when closing
154
- scale.value = 1
155
- panPosition.x = 0
156
- panPosition.y = 0
157
- isFullscreen.value = false
158
- showMobileThumbnails.value = false
159
-
160
80
  if (!import.meta.env.SSR) {
161
81
  document.removeEventListener('keydown', handleKeyboardInput)
162
82
  document.removeEventListener('keyup', handleKeyboardRelease)
163
- document.removeEventListener('keydown', handleTabKey)
164
83
  }
165
84
  }
166
85
  isGalleryOpen.value = value
@@ -177,13 +96,7 @@ function openGalleryImage(index: number | undefined) {
177
96
  }
178
97
 
179
98
  function goNextImage() {
180
- // Reset zoom and pan when navigating
181
- scale.value = 1
182
- panPosition.x = 0
183
- panPosition.y = 0
184
-
185
99
  direction.value = 'next'
186
- isImageLoading.value = true
187
100
  if (modelValue.value < props.images.length - 1) {
188
101
  modelValue.value++
189
102
  }
@@ -193,13 +106,7 @@ function goNextImage() {
193
106
  }
194
107
 
195
108
  function goPrevImage() {
196
- // Reset zoom and pan when navigating
197
- scale.value = 1
198
- panPosition.x = 0
199
- panPosition.y = 0
200
-
201
109
  direction.value = 'prev'
202
- isImageLoading.value = true
203
110
  if (modelValue.value > 0) {
204
111
  modelValue.value--
205
112
  }
@@ -217,159 +124,41 @@ const modelValueSrc = computed(() => {
217
124
 
218
125
  const start = reactive({ x: 0, y: 0 })
219
126
 
220
- // Calculate the distance between two touch points
221
- function getDistance(touch1: Touch, touch2: Touch): number {
222
- const dx = touch1.clientX - touch2.clientX
223
- const dy = touch1.clientY - touch2.clientY
224
- return Math.sqrt(dx * dx + dy * dy)
225
- }
226
-
227
- // Calculate the center point between two touches
228
- function getTouchCenter(touch1: Touch, touch2: Touch, element: HTMLElement) {
229
- const rect = element.getBoundingClientRect()
230
- const centerX = (touch1.clientX + touch2.clientX) / 2
231
- const centerY = (touch1.clientY + touch2.clientY) / 2
232
-
233
- return {
234
- x: (centerX - rect.left) / rect.width,
235
- y: (centerY - rect.top) / rect.height,
236
- }
237
- }
238
-
239
127
  function touchStart(event: TouchEvent) {
240
- const now = Date.now()
241
- const container = event.currentTarget as HTMLElement
128
+ const touch = event.touches[0]
129
+ const targetElement = touch.target as HTMLElement
242
130
 
243
- // Handle double tap detection for zoom
244
- if (now - lastTapTime.value < 300 && event.touches.length === 1) {
245
- if (scale.value !== 1) {
246
- // Reset zoom and pan on double tap if already zoomed
247
- scale.value = 1
248
- panPosition.x = 0
249
- panPosition.y = 0
250
- }
251
- else {
252
- // Zoom to a moderate level on double tap
253
- scale.value = 2.5
254
- // Center zoom on tap location
255
- const touch = event.touches[0]
256
- const rect = container.getBoundingClientRect()
257
- touchCenter.x = (touch.clientX - rect.left) / rect.width
258
- touchCenter.y = (touch.clientY - rect.top) / rect.height
259
- }
260
- event.preventDefault()
261
- return
131
+ // Check if the touch started on an interactive element
132
+ if (targetElement.closest('button, a, input, textarea, select')) {
133
+ return // Don't handle swipe if interacting with an interactive element
262
134
  }
263
135
 
264
- lastTapTime.value = now
265
-
266
- // Detect if we're starting to pinch zoom
267
- if (event.touches.length === 2) {
268
- isPinching.value = true
269
- startDistance.value = getDistance(event.touches[0], event.touches[1])
270
- const center = getTouchCenter(event.touches[0], event.touches[1], container)
271
- touchCenter.x = center.x
272
- touchCenter.y = center.y
273
- event.preventDefault()
274
- return
275
- }
276
-
277
- // If we're already zoomed in, enable panning with one finger
278
- if (scale.value > 1 && event.touches.length === 1) {
279
- isPanning.value = true
280
- startPanPosition.x = panPosition.x
281
- startPanPosition.y = panPosition.y
282
- start.x = event.touches[0].clientX
283
- start.y = event.touches[0].clientY
284
- event.preventDefault()
285
- return
286
- }
287
-
288
- // Normal swipe behavior if not zoomed
289
- if (scale.value <= 1 && event.touches.length === 1) {
290
- const touch = event.touches[0]
291
- const targetElement = touch.target as HTMLElement
292
-
293
- // Check if the touch started on an interactive element
294
- if (targetElement.closest('button, a, input, textarea, select')) {
295
- return // Don't handle swipe if interacting with an interactive element
296
- }
297
-
298
- start.x = touch.screenX
299
- start.y = touch.screenY
300
- }
136
+ start.x = touch.screenX
137
+ start.y = touch.screenY
301
138
  }
302
-
303
- function touchMove(event: TouchEvent) {
304
- // Handle pinch zoom
305
- if (isPinching.value && event.touches.length === 2) {
306
- const currentDistance = getDistance(event.touches[0], event.touches[1])
307
- const newScale = (currentDistance / startDistance.value) * scale.value
308
-
309
- // Limit zoom range
310
- scale.value = Math.min(Math.max(0.5, newScale), 5)
311
- event.preventDefault()
312
- return
313
- }
314
-
315
- // Handle panning when zoomed in
316
- if (isPanning.value && event.touches.length === 1) {
317
- const deltaX = event.touches[0].clientX - start.x
318
- const deltaY = event.touches[0].clientY - start.y
319
-
320
- // Calculate how much the pan should be constrained based on zoom level
321
- const constraint = Math.max(0, scale.value - 1) * 150
322
-
323
- panPosition.x = Math.min(Math.max(startPanPosition.x + deltaX, -constraint), constraint)
324
- panPosition.y = Math.min(Math.max(startPanPosition.y + deltaY, -constraint), constraint)
325
-
326
- event.preventDefault()
327
- }
328
- }
329
-
330
139
  function touchEnd(event: TouchEvent) {
331
- // Reset pinch zoom state
332
- if (isPinching.value) {
333
- isPinching.value = false
334
- return
335
- }
140
+ const touch = event.changedTouches[0]
141
+ const targetElement = touch.target as HTMLElement
336
142
 
337
- // Reset panning state
338
- if (isPanning.value) {
339
- isPanning.value = false
340
- return
143
+ // Check if the touch ended on an interactive element
144
+ if (targetElement.closest('button, a, input, textarea, select')) {
145
+ return // Don't handle swipe if interacting with an interactive element
341
146
  }
342
147
 
343
- // If we're zoomed in, don't handle swiping between images
344
- if (scale.value > 1) {
345
- return
346
- }
148
+ const end = { x: touch.screenX, y: touch.screenY }
347
149
 
348
- // Normal swipe behavior if not zoomed
349
- if (event.changedTouches.length === 1) {
350
- const touch = event.changedTouches[0]
351
- const targetElement = touch.target as HTMLElement
150
+ const diffX = start.x - end.x
151
+ const diffY = start.y - end.y
352
152
 
353
- // Check if the touch ended on an interactive element
354
- if (targetElement.closest('button, a, input, textarea, select')) {
355
- return // Don't handle swipe if interacting with an interactive element
153
+ // Add a threshold to prevent accidental swipes
154
+ if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
155
+ if (diffX > 0) {
156
+ direction.value = 'next'
157
+ goNextImage()
356
158
  }
357
-
358
- const end = { x: touch.screenX, y: touch.screenY }
359
-
360
- const diffX = start.x - end.x
361
- const diffY = start.y - end.y
362
-
363
- // Add a threshold to prevent accidental swipes
364
- if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
365
- if (diffX > 0) {
366
- direction.value = 'next'
367
- goNextImage()
368
- }
369
- else {
370
- direction.value = 'prev'
371
- goPrevImage()
372
- }
159
+ else {
160
+ direction.value = 'prev'
161
+ goPrevImage()
373
162
  }
374
163
  }
375
164
  }
@@ -389,25 +178,8 @@ function handleKeyboardInput(event: KeyboardEvent) {
389
178
 
390
179
  switch (event.key) {
391
180
  case 'Escape':
392
- if (scale.value > 1) {
393
- // If zoomed in, reset zoom first
394
- scale.value = 1
395
- panPosition.x = 0
396
- panPosition.y = 0
397
- }
398
- else if (showMobileThumbnails.value) {
399
- // If mobile thumbnails are shown, hide them first
400
- showMobileThumbnails.value = false
401
- }
402
- else if (isFullscreen.value) {
403
- // If in fullscreen, exit fullscreen first
404
- isFullscreen.value = false
405
- }
406
- else {
407
- // Otherwise close the gallery
408
- event.preventDefault()
409
- setModal(false)
410
- }
181
+ event.preventDefault()
182
+ setModal(false)
411
183
  break
412
184
  case 'ArrowRight':
413
185
  isKeyPressed.value = true
@@ -419,9 +191,6 @@ function handleKeyboardInput(event: KeyboardEvent) {
419
191
  direction.value = 'prev'
420
192
  goPrevImage()
421
193
  break
422
- case 'f':
423
- toggleFullscreen()
424
- break
425
194
  default:
426
195
  break
427
196
  }
@@ -433,10 +202,6 @@ function handleKeyboardRelease(event: KeyboardEvent) {
433
202
  }
434
203
  }
435
204
 
436
- function onImageLoad() {
437
- isImageLoading.value = false
438
- }
439
-
440
205
  function closeGallery() {
441
206
  setModal(false)
442
207
  }
@@ -448,21 +213,6 @@ function handleBackdropClick(event: MouseEvent) {
448
213
  }
449
214
  }
450
215
 
451
- // Toggle mobile thumbnails panel
452
- function toggleMobileThumbnails() {
453
- showMobileThumbnails.value = !showMobileThumbnails.value
454
- }
455
-
456
- // Computed transform style for zoom and pan
457
- const imageTransform = computed(() => {
458
- return `scale(${scale.value}) translate(${panPosition.x / scale.value}px, ${panPosition.y / scale.value}px)`
459
- })
460
-
461
- // Transform origin for zoom based on touch position
462
- const imageTransformOrigin = computed(() => {
463
- return `${touchCenter.x * 100}% ${touchCenter.y * 100}%`
464
- })
465
-
466
216
  onMounted(() => {
467
217
  eventBus.on(`${props.id}GalleryImage`, openGalleryImage)
468
218
  eventBus.on(`${props.id}Gallery`, openGalleryImage)
@@ -476,7 +226,6 @@ onUnmounted(() => {
476
226
  if (!import.meta.env.SSR) {
477
227
  document.removeEventListener('keydown', handleKeyboardInput)
478
228
  document.removeEventListener('keyup', handleKeyboardRelease)
479
- document.removeEventListener('keydown', handleTabKey)
480
229
  document.body.style.overflow = '' // Ensure body scrolling is restored
481
230
  }
482
231
  })
@@ -494,100 +243,58 @@ onUnmounted(() => {
494
243
  >
495
244
  <div
496
245
  v-if="isGalleryOpen"
497
- ref="galleryRef"
498
- class="fixed bg-gradient-to-b from-fv-neutral-950 to-fv-neutral-900 text-white inset-0 max-w-[100vw] overflow-hidden"
499
- :class="{ 'fullscreen-mode': isFullscreen }"
246
+ class="fixed bg-fv-neutral-900 text-white inset-0 max-w-[100vw] overflow-y-auto overflow-x-hidden"
500
247
  style="z-index: 37"
501
248
  role="dialog"
502
249
  aria-modal="true"
503
250
  @click="handleBackdropClick"
504
251
  >
505
252
  <div
506
- class="relative w-full max-w-full flex flex-col justify-center items-center h-full"
253
+ class="relative w-full max-w-full flex flex-col justify-center items-center"
507
254
  style="z-index: 38"
508
255
  @click.stop
509
256
  >
510
- <div class="flex flex-grow gap-4 w-full max-w-full h-full">
511
- <div class="flex-grow h-full flex items-center relative">
512
- <!-- Top control bar -->
513
- <div class="absolute top-0 left-0 right-0 flex justify-between items-center p-4 bg-gradient-to-b from-black/50 to-transparent z-[40]">
514
- <div class="flex items-center gap-2">
515
- <button
516
- aria-label="Close gallery"
517
- class="rounded-full p-2 bg-black/40 backdrop-blur-sm hover:bg-black/60 transition-colors"
518
- @click="setModal(false)"
519
- >
520
- <component :is="closeIcon" class="w-6 h-6" />
521
- </button>
522
-
523
- <!-- Mobile thumbnails toggle button -->
524
- <button
525
- v-if="images.length > 1"
526
- aria-label="Toggle thumbnails"
527
- class="sm:hidden rounded-full p-2 bg-black/40 backdrop-blur-sm hover:bg-black/60 transition-colors"
528
- @click="toggleMobileThumbnails"
529
- >
530
- <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
531
- <path d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
532
- </svg>
533
- </button>
534
-
535
- <!-- Fullscreen toggle button -->
536
- <button
537
- aria-label="Toggle fullscreen"
538
- class="rounded-full p-2 bg-black/40 backdrop-blur-sm hover:bg-black/60 transition-colors"
539
- @click="toggleFullscreen"
540
- >
541
- <ArrowsPointingOutIcon class="w-5 h-5" />
542
- </button>
543
- </div>
544
-
545
- <div class="px-3 py-1 rounded-full bg-black/40 backdrop-blur-sm text-white text-sm">
546
- {{ imageCounter }}
547
- </div>
548
- </div>
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>
549
267
 
550
268
  <div
551
- class="flex h-full relative flex-grow items-center justify-center gap-2 z-[1]"
269
+ class="flex h-[100vh] relative flex-grow items-center justify-center gap-2 z-[1]"
552
270
  >
553
- <!-- Left nav button (larger touch area on mobile) -->
554
271
  <div
555
- class="absolute left-0 top-0 bottom-0 w-1/3 flex items-center justify-start pl-4 lg:pl-10 z-[2]"
556
- @click="scale <= 1 && goPrevImage()"
272
+ class="hidden lg:relative z-[2] lg:flex w-10 flex-shrink-0 items-center justify-center flex-0"
557
273
  >
558
274
  <button
559
275
  v-if="images.length > 1"
276
+ class="btn p-1 rounded-full"
560
277
  aria-label="Previous image"
561
- class="rounded-full p-2 bg-black/40 backdrop-blur-sm hover:bg-black/60 transition-colors hidden sm:block"
562
- @click.stop
278
+ @click="goPrevImage()"
563
279
  >
564
- <ArrowLeftCircleIcon class="w-6 h-6" />
280
+ <ArrowLeftCircleIcon class="w-8 h-8" />
565
281
  </button>
566
282
  </div>
567
-
568
283
  <div
569
284
  class="flex-1 flex flex-col z-[2] items-center justify-center max-w-full lg:max-w-[calc(100vw - 256px)] relative"
570
285
  >
571
- <!-- Loading indicator -->
572
- <div
573
- v-if="isImageLoading"
574
- class="absolute inset-0 flex items-center justify-center z-10 bg-fv-neutral-900/30 backdrop-blur-sm"
575
- >
576
- <div class="w-10 h-10 border-t-2 border-white rounded-full animate-spin" />
577
- </div>
578
-
579
286
  <transition
580
287
  :name="direction === 'next' ? 'slide-next' : 'slide-prev'"
581
288
  mode="out-in"
582
289
  >
583
290
  <div
291
+ v-if="true"
584
292
  :key="`image-display-${modelValue}`"
585
293
  class="flex-1 w-full max-w-full flex flex-col items-center justify-center absolute inset-0 z-[2]"
586
294
  >
587
295
  <div
588
- class="flex-1 w-full max-w-full flex items-center justify-center overflow-hidden"
296
+ class="flex-1 w-full max-w-full flex items-center justify-center"
589
297
  @touchstart="touchStart"
590
- @touchmove="touchMove"
591
298
  @touchend="touchEnd"
592
299
  >
593
300
  <template
@@ -597,89 +304,63 @@ onUnmounted(() => {
597
304
  <component
598
305
  :is="videoComponent"
599
306
  :src="isVideo(images[modelValue])"
600
- class="shadow-2xl max-w-full h-auto object-contain max-h-[85vh]"
307
+ class="shadow max-w-full h-auto object-contain max-h-[85vh]"
601
308
  />
602
309
  </ClientOnly>
603
310
  </template>
604
311
  <template v-else>
605
- <div class="image-container">
606
- <img
607
- v-if="modelValueSrc && imageComponent === 'img'"
608
- class="shadow-2xl max-w-full h-auto object-contain max-h-[80vh] transition-transform duration-100 rounded-md"
609
- :class="{ 'cursor-grab': scale > 1, 'cursor-zoom-in': scale === 1 }"
610
- :style="{
611
- transform: imageTransform,
612
- transformOrigin: imageTransformOrigin,
613
- touchAction: scale > 1 ? 'none' : 'auto',
614
- }"
615
- :src="modelValueSrc"
616
- :alt="`Gallery image ${modelValue + 1}`"
617
- @load="onImageLoad"
618
- >
619
- <component
620
- :is="imageComponent"
621
- v-else-if="modelValueSrc && imageComponent"
622
- :image="modelValueSrc.image"
623
- :variant="modelValueSrc.variant"
624
- :alt="modelValueSrc.alt"
625
- class="shadow-2xl max-w-full h-auto object-contain max-h-[80vh] transition-transform duration-100 rounded-md"
626
- :style="{
627
- transform: imageTransform,
628
- transformOrigin: imageTransformOrigin,
629
- touchAction: scale > 1 ? 'none' : 'auto',
630
- }"
631
- @load="onImageLoad"
632
- />
633
- </div>
312
+ <img
313
+ v-if="modelValueSrc && imageComponent === 'img'"
314
+ class="shadow max-w-full h-auto object-contain max-h-[85vh]"
315
+ :src="modelValueSrc"
316
+ :alt="`Gallery image ${modelValue + 1}`"
317
+ >
318
+ <component
319
+ :is="imageComponent"
320
+ v-else-if="modelValueSrc && imageComponent"
321
+ :image="modelValueSrc.image"
322
+ :variant="modelValueSrc.variant"
323
+ :alt="modelValueSrc.alt"
324
+ class="shadow max-w-full h-auto object-contain max-h-[85vh]"
325
+ />
634
326
  </template>
635
327
  </div>
636
-
637
- <!-- Mobile zoom hint -->
638
328
  <div
639
- v-if="scale === 1 && !isImageLoading"
640
- class="text-xs text-white/60 p-2 sm:hidden"
641
- >
642
- Double-tap to zoom
643
- </div>
644
-
645
- <div
646
- class="flex-0 py-4 flex items-center justify-center max-w-full w-full relative !z-[3]"
329
+ class="flex-0 py-2 flex items-center justify-center max-w-full w-full relative !z-[3]"
647
330
  >
648
331
  <slot :value="images[modelValue]" />
649
332
  </div>
650
333
  </div>
651
334
  </transition>
652
335
  </div>
653
-
654
- <!-- Right nav button (larger touch area on mobile) -->
655
336
  <div
656
- class="absolute right-0 top-0 bottom-0 w-1/3 flex items-center justify-end pr-4 lg:pr-10 z-[2]"
657
- @click="scale <= 1 && goNextImage()"
337
+ class="hidden lg:flex w-10 flex-shrink-0 items-center justify-center"
658
338
  >
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>
659
352
  <button
660
353
  v-if="images.length > 1"
354
+ class="btn p-1 rounded-full"
661
355
  aria-label="Next image"
662
- class="rounded-full p-2 bg-black/40 backdrop-blur-sm hover:bg-black/60 transition-colors hidden sm:block"
663
- @click.stop
356
+ @click="goNextImage()"
664
357
  >
665
- <ArrowRightCircleIcon class="w-6 h-6" />
358
+ <ArrowRightCircleIcon class="w-8 h-8" />
666
359
  </button>
667
360
  </div>
668
-
669
- <!-- Sidebar toggle button (desktop) -->
670
- <button
671
- class="absolute top-4 right-4 rounded-full p-2 bg-black/40 backdrop-blur-sm hover:bg-black/60 transition-colors hidden lg:block"
672
- style="z-index: 39"
673
- :aria-label="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
674
- @click="() => (sidePanel = !sidePanel)"
675
- >
676
- <ChevronDoubleRightIcon v-if="sidePanel" class="w-5 h-5" />
677
- <ChevronDoubleLeftIcon v-else class="w-5 h-5" />
678
- </button>
679
361
  </div>
680
362
  </div>
681
363
 
682
- <!-- Sidebar with thumbnails (desktop) -->
683
364
  <transition
684
365
  enter-active-class="transform transition ease-in-out duration-300"
685
366
  enter-from-class="translate-x-full"
@@ -690,162 +371,59 @@ onUnmounted(() => {
690
371
  >
691
372
  <div
692
373
  v-if="sidePanel"
693
- class="hidden lg:block flex-shrink-0 w-64 bg-fv-neutral-800/80 backdrop-blur-sm h-full max-h-full overflow-y-auto border-l border-fv-neutral-700/50"
374
+ class="hidden lg:block flex-shrink-0 w-64 bg-fv-neutral-800 h-[100vh] max-h-[100vh] overflow-y-auto"
694
375
  >
695
- <!-- Sidebar header -->
696
- <div class="p-4 border-b border-fv-neutral-700/50">
697
- <h3 class="text-lg font-medium">
698
- Gallery
699
- </h3>
700
- </div>
701
-
702
- <!-- Paging controls if provided -->
703
- <div v-if="paging" class="flex items-center justify-center p-2 border-b border-fv-neutral-700/50">
376
+ <!-- Side panel content -->
377
+ <div v-if="paging" class="flex items-center justify-center">
704
378
  <DefaultPaging :id="id" :items="paging" />
705
379
  </div>
706
-
707
- <!-- Thumbnails grid -->
708
- <div class="grid grid-cols-2 gap-2 p-4">
380
+ <div class="grid grid-cols-2 gap-2 p-2">
709
381
  <div
710
382
  v-for="i in images.length"
711
383
  :key="`bg_${id}_${i}`"
712
- class="relative group"
384
+ class="hover:!brightness-100"
385
+ :style="{
386
+ filter:
387
+ i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.5)',
388
+ }"
713
389
  >
714
- <!-- Active image indicator -->
715
- <div
716
- v-if="i - 1 === modelValue"
717
- class="absolute inset-0 border-2 border-white rounded-lg z-10"
718
- />
719
-
720
- <!-- Thumbnail -->
721
- <div
722
- class="overflow-hidden rounded-lg transition-all duration-200"
723
- :class="{ 'ring-2 ring-white': i - 1 === modelValue, 'opacity-60 group-hover:opacity-100': i - 1 !== modelValue }"
724
- >
725
- <img
726
- v-if="imageComponent === 'img'"
727
- class="h-auto max-w-full rounded-lg cursor-pointer shadow transform transition duration-300 group-hover:scale-105 group-hover:brightness-110"
728
- :src="getThumbnailUrl(images[i - 1])"
729
- :alt="`Thumbnail ${i}`"
730
- @click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
731
- >
732
- <component
733
- :is="imageComponent"
734
- v-else
735
- :image="getThumbnailUrl(images[i - 1]).image"
736
- :variant="getThumbnailUrl(images[i - 1]).variant"
737
- :alt="getThumbnailUrl(images[i - 1]).alt"
738
- :class="`h-auto max-w-full rounded-lg cursor-pointer shadow transform transition duration-300 group-hover:scale-105 group-hover:brightness-110 ${getBorderColor(images[i - 1])}`"
739
- @click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
740
- />
741
- </div>
742
- </div>
743
- </div>
744
- </div>
745
- </transition>
746
- </div>
747
-
748
- <!-- Mobile navigation controls (bottom bar) -->
749
- <div
750
- v-if="!showMobileThumbnails"
751
- class="fixed bottom-0 left-0 right-0 sm:hidden flex justify-between items-center p-4 bg-gradient-to-t from-black/80 to-transparent"
752
- style="z-index: 40"
753
- >
754
- <button
755
- v-if="images.length > 1 && scale <= 1"
756
- aria-label="Previous image"
757
- class="rounded-full p-3 bg-black/50 backdrop-blur-sm"
758
- @click="goPrevImage"
759
- >
760
- <ArrowLeftCircleIcon class="w-6 h-6" />
761
- </button>
762
-
763
- <!-- Reset zoom button when zoomed -->
764
- <button
765
- v-if="scale > 1"
766
- aria-label="Reset zoom"
767
- class="rounded-full p-2 bg-black/50 backdrop-blur-sm"
768
- @click="scale = 1; panPosition.x = 0; panPosition.y = 0"
769
- >
770
- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
771
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5" />
772
- </svg>
773
- </button>
774
-
775
- <button
776
- v-if="images.length > 1 && scale <= 1"
777
- aria-label="Next image"
778
- class="rounded-full p-3 bg-black/50 backdrop-blur-sm"
779
- @click="goNextImage"
780
- >
781
- <ArrowRightCircleIcon class="w-6 h-6" />
782
- </button>
783
- </div>
784
-
785
- <!-- Mobile thumbnails panel -->
786
- <transition
787
- enter-active-class="transform transition ease-in-out duration-300"
788
- enter-from-class="translate-y-full"
789
- enter-to-class="translate-y-0"
790
- leave-active-class="transform transition ease-in-out duration-300"
791
- leave-from-class="translate-y-0"
792
- leave-to-class="translate-y-full"
793
- >
794
- <div
795
- v-if="showMobileThumbnails"
796
- class="fixed bottom-0 left-0 right-0 sm:hidden bg-black/80 backdrop-blur-md pt-4 pb-6 border-t border-fv-neutral-700/50"
797
- style="z-index: 50"
798
- >
799
- <div class="relative">
800
- <!-- Close thumbnails button -->
801
- <button
802
- aria-label="Close thumbnails"
803
- class="absolute top-0 right-4 rounded-full p-1 bg-black/40 hover:bg-black/60"
804
- @click="showMobileThumbnails = false"
805
- >
806
- <XCircleIcon class="w-5 h-5" />
807
- </button>
808
-
809
- <p class="text-center text-sm mb-2 font-medium">
810
- Thumbnails
811
- </p>
812
-
813
- <!-- Horizontal scrollable thumbnails -->
814
- <div class="flex overflow-x-auto px-4 pb-2 space-x-2 hide-scrollbar">
815
- <div
816
- v-for="i in images.length"
817
- :key="`mt_${id}_${i}`"
818
- class="flex-shrink-0 w-16 h-16 relative"
819
- :class="{ 'opacity-100': i - 1 === modelValue, 'opacity-50': i - 1 !== modelValue }"
820
- >
821
- <div
822
- v-if="i - 1 === modelValue"
823
- class="absolute inset-0 border-2 border-white rounded-lg z-10"
824
- />
825
390
  <img
826
- class="h-full w-full object-cover rounded-lg"
391
+ v-if="imageComponent === 'img'"
392
+ :class="`h-auto max-w-full rounded-lg cursor-pointer shadow ${getBorderColor(
393
+ images[i - 1],
394
+ )}`"
827
395
  :src="getThumbnailUrl(images[i - 1])"
828
396
  :alt="`Thumbnail ${i}`"
829
- @click="$eventBus.emit(`${id}GalleryImage`, i - 1); showMobileThumbnails = false"
397
+ @click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
830
398
  >
399
+ <component
400
+ :is="imageComponent"
401
+ v-else
402
+ :image="getThumbnailUrl(images[i - 1]).image"
403
+ :variant="getThumbnailUrl(images[i - 1]).variant"
404
+ :alt="getThumbnailUrl(images[i - 1]).alt"
405
+ :class="`h-auto max-w-full rounded-lg cursor-pointer shadow ${getBorderColor(
406
+ images[i - 1],
407
+ )}`"
408
+ @click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
409
+ />
831
410
  </div>
832
411
  </div>
833
412
  </div>
834
- </div>
835
- </transition>
413
+ </transition>
414
+ </div>
836
415
  </div>
837
416
  </div>
838
417
  </transition>
839
418
 
840
- <!-- Grid view mode -->
841
419
  <div v-if="mode === 'grid' || mode === 'mason' || mode === 'custom'" class="min-h-[600px]">
842
420
  <div
843
421
  :class="{
844
- 'grid grid-cols-2 md:grid-cols-4 xl:grid-cols-6 gap-4 items-start':
422
+ ' grid grid-cols-2 md:grid-cols-4 xl:grid-cols-6 gap-4 items-start':
845
423
  mode === 'mason',
846
- 'grid grid-cols-2 md:grid-cols-4 xl:grid-cols-6 gap-4 items-center':
424
+ ' grid grid-cols-2 md:grid-cols-4 xl:grid-cols-6 gap-4 items-center':
847
425
  mode === 'grid',
848
- 'custom-grid': mode === 'custom',
426
+ ' custom-grid': mode === 'custom',
849
427
  }"
850
428
  >
851
429
  <slot name="thumbnail" />
@@ -860,19 +438,10 @@ onUnmounted(() => {
860
438
  </div>
861
439
 
862
440
  <template v-for="j in gridHeight" :key="`gi_${id}_${i + j}`">
863
- <div class="group overflow-hidden rounded-lg relative">
864
- <!-- Visual feedback for the tap action on mobile -->
865
- <div class="absolute inset-0 bg-black/30 opacity-0 transition-opacity duration-300 flex items-center justify-center z-[1] group-hover-opacity-100 group-active-opacity-100">
866
- <div class="bg-black/60 rounded-full p-2 transform scale-0 transition-transform duration-300 group-hover-scale-100 group-active-scale-100">
867
- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
868
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" />
869
- </svg>
870
- </div>
871
- </div>
872
-
441
+ <div>
873
442
  <img
874
443
  v-if="i + j - 2 < images.length && imageComponent === 'img'"
875
- class="h-auto max-w-full rounded-lg cursor-pointer transform transition-transform duration-300 group-hover-scale-105 group-active-scale-102"
444
+ class="h-auto max-w-full rounded-lg cursor-pointer"
876
445
  :src="getThumbnailUrl(images[i + j - 2])"
877
446
  :alt="`Gallery image ${i + j - 1}`"
878
447
  @click="$eventBus.emit(`${id}GalleryImage`, i + j - 2)"
@@ -883,7 +452,9 @@ onUnmounted(() => {
883
452
  :image="getThumbnailUrl(images[i + j - 2]).image"
884
453
  :variant="getThumbnailUrl(images[i + j - 2]).variant"
885
454
  :alt="getThumbnailUrl(images[i + j - 2]).alt"
886
- :class="`h-auto max-w-full rounded-lg cursor-pointer transform transition-transform duration-300 group-hover-scale-105 group-active-scale-102 ${getBorderColor(images[i + j - 2])}`"
455
+ :class="`h-auto max-w-full rounded-lg cursor-pointer ${getBorderColor(
456
+ images[i + j - 2],
457
+ )}`"
887
458
  :likes="getThumbnailUrl(images[i + j - 2]).likes"
888
459
  :show-likes="getThumbnailUrl(images[i + j - 2]).showLikes"
889
460
  :is-author="getThumbnailUrl(images[i + j - 2]).isAuthor"
@@ -894,23 +465,13 @@ onUnmounted(() => {
894
465
  </template>
895
466
  </div>
896
467
  </template>
897
- <div v-else class="relative group overflow-hidden rounded-lg">
898
- <div v-if="ranking" class="img-gallery-ranking z-10">
468
+ <div v-else class="relative">
469
+ <div v-if="ranking" class="img-gallery-ranking">
899
470
  {{ i }}
900
471
  </div>
901
-
902
- <!-- Visual overlay on hover with touch feedback -->
903
- <div class="absolute inset-0 bg-black/30 opacity-0 transition-opacity duration-300 flex items-center justify-center z-[1] group-hover-opacity-100 group-active-opacity-100">
904
- <div class="bg-black/60 rounded-full p-2 transform scale-0 transition-transform duration-300 group-hover-scale-100 group-active-scale-100">
905
- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
906
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" />
907
- </svg>
908
- </div>
909
- </div>
910
-
911
472
  <img
912
473
  v-if="imageComponent === 'img'"
913
- class="h-auto max-w-full rounded-lg cursor-pointer transform transition-transform duration-300 group-hover-scale-105 group-active-scale-102"
474
+ class="h-auto max-w-full rounded-lg cursor-pointer"
914
475
  :src="getThumbnailUrl(images[i - 1])"
915
476
  :alt="`Gallery image ${i}`"
916
477
  @click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
@@ -921,7 +482,9 @@ onUnmounted(() => {
921
482
  :image="getThumbnailUrl(images[i - 1]).image"
922
483
  :variant="getThumbnailUrl(images[i - 1]).variant"
923
484
  :alt="getThumbnailUrl(images[i - 1]).alt"
924
- :class="`h-auto max-w-full rounded-lg cursor-pointer transform transition-transform duration-300 group-hover-scale-105 group-active-scale-102 ${getBorderColor(images[i - 1])}`"
485
+ :class="`h-auto max-w-full rounded-lg cursor-pointer ${getBorderColor(
486
+ images[i - 1],
487
+ )}`"
925
488
  :likes="getThumbnailUrl(images[i - 1]).likes"
926
489
  :show-likes="getThumbnailUrl(images[i - 1]).showLikes"
927
490
  :is-author="getThumbnailUrl(images[i - 1]).isAuthor"
@@ -932,16 +495,11 @@ onUnmounted(() => {
932
495
  </template>
933
496
  </div>
934
497
  </div>
935
-
936
- <!-- Button mode -->
937
498
  <button
938
499
  v-if="mode === 'button'"
939
- :class="`btn ${buttonType ? buttonType : 'primary'} defaults flex items-center gap-2`"
500
+ :class="`btn ${buttonType ? buttonType : 'primary'} defaults`"
940
501
  @click="openGalleryImage(0)"
941
502
  >
942
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
943
- <path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd" />
944
- </svg>
945
503
  {{ buttonText ? buttonText : $t("open_gallery_cta") }}
946
504
  </button>
947
505
  </div>
@@ -952,9 +510,9 @@ onUnmounted(() => {
952
510
  .slide-next-enter-active,
953
511
  .slide-next-leave-active {
954
512
  transition:
955
- opacity 0.3s,
956
- transform 0.3s,
957
- filter 0.3s;
513
+ opacity 0.15s,
514
+ transform 0.15s,
515
+ filter 0.15s;
958
516
  }
959
517
 
960
518
  .slide-next-enter-from {
@@ -985,9 +543,9 @@ onUnmounted(() => {
985
543
  .slide-prev-enter-active,
986
544
  .slide-prev-leave-active {
987
545
  transition:
988
- opacity 0.3s,
989
- transform 0.3s,
990
- filter 0.3s;
546
+ opacity 0.15s,
547
+ transform 0.15s,
548
+ filter 0.15s;
991
549
  }
992
550
 
993
551
  .slide-prev-enter-from {
@@ -1014,86 +572,18 @@ onUnmounted(() => {
1014
572
  filter: blur(10px);
1015
573
  }
1016
574
 
1017
- /* Custom animation for loading spinner */
1018
- @keyframes spin {
1019
- from { transform: rotate(0deg); }
1020
- to { transform: rotate(360deg); }
1021
- }
1022
-
1023
- .animate-spin {
1024
- animation: spin 1s linear infinite;
575
+ /* Ensure the images are positioned correctly to prevent overlap */
576
+ .relative-container {
577
+ position: relative;
578
+ width: 100%;
579
+ height: 100%;
1025
580
  }
1026
581
 
1027
- /* Improved gallery ranking */
1028
- .img-gallery-ranking {
582
+ .relative-container > div {
1029
583
  position: absolute;
1030
- top: 0.5rem;
1031
- left: 0.5rem;
1032
- width: 1.5rem;
1033
- height: 1.5rem;
1034
- display: flex;
1035
- align-items: center;
1036
- justify-content: center;
1037
- background-color: rgba(0, 0, 0, 0.7);
1038
- color: white;
1039
- border-radius: 9999px;
1040
- font-size: 0.75rem;
1041
- font-weight: 600;
1042
- z-index: 5;
1043
- }
1044
-
1045
- /* Image container */
1046
- .image-container {
1047
- display: flex;
1048
- align-items: center;
1049
- justify-content: center;
584
+ top: 0;
585
+ left: 0;
1050
586
  width: 100%;
1051
587
  height: 100%;
1052
- user-select: none;
1053
- touch-action: none;
1054
- }
1055
-
1056
- /* Hide scrollbar for mobile thumbnails */
1057
- .hide-scrollbar {
1058
- -ms-overflow-style: none; /* IE and Edge */
1059
- scrollbar-width: none; /* Firefox */
1060
- }
1061
-
1062
- .hide-scrollbar::-webkit-scrollbar {
1063
- display: none; /* Chrome, Safari, Opera */
1064
- }
1065
-
1066
- /* Use direct CSS for hover/active states - avoid @apply for these */
1067
- .group:hover .group-hover-opacity-100 {
1068
- opacity: 1;
1069
- }
1070
-
1071
- .group:hover .group-hover-scale-100 {
1072
- transform: scale(1);
1073
- }
1074
-
1075
- .group:hover .group-hover-scale-105 {
1076
- transform: scale(1.05);
1077
- }
1078
-
1079
- .group:active .group-active-opacity-100 {
1080
- opacity: 1;
1081
- }
1082
-
1083
- .group:active .group-active-scale-100 {
1084
- transform: scale(1);
1085
- }
1086
-
1087
- .group:active .group-active-scale-102 {
1088
- transform: scale(1.02);
1089
- }
1090
-
1091
- /* Fullscreen mode */
1092
- .fullscreen-mode {
1093
- position: fixed;
1094
- inset: 0;
1095
- width: 100vw;
1096
- height: 100vh;
1097
- z-index: 99;
1098
588
  }
1099
589
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fy-/fws-vue",
3
- "version": "2.2.48",
3
+ "version": "2.2.49",
4
4
  "author": "Florian 'Fy' Gasquez <m@fy.to>",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/fy-to/FWJS#readme",