@fy-/fws-vue 2.3.21 → 2.3.23
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.
|
@@ -46,8 +46,6 @@ const { width: windowWidth, height: windowHeight } = useWindowSize()
|
|
|
46
46
|
const { height: topControlsHeight } = useElementSize(topControlsRef)
|
|
47
47
|
const { height: infoPanelHeight } = useElementSize(infoPanelRef)
|
|
48
48
|
|
|
49
|
-
// We no longer need derived measurements as we use CSS variables instead
|
|
50
|
-
|
|
51
49
|
// Use VueUse's useFullscreen for better fullscreen handling
|
|
52
50
|
const { isFullscreen: isElementFullscreen, enter: enterFullscreen, exit: exitFullscreen } = useFullscreen(galleryRef)
|
|
53
51
|
|
|
@@ -131,33 +129,27 @@ const currentImage = computed(() => {
|
|
|
131
129
|
const imageCount = computed(() => props.images.length)
|
|
132
130
|
const currentIndex = computed(() => modelValue.value + 1)
|
|
133
131
|
|
|
134
|
-
//
|
|
135
|
-
function updateInfoHeight() {
|
|
136
|
-
if (!infoPanelRef.value) return
|
|
137
|
-
|
|
138
|
-
const height = infoPanelRef.value.offsetHeight || 0
|
|
139
|
-
document.documentElement.style.setProperty('--info-height', `${height}px`)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function updateControlsHeight() {
|
|
143
|
-
if (!topControlsRef.value) return
|
|
144
|
-
|
|
145
|
-
const height = topControlsRef.value.offsetHeight || 0
|
|
146
|
-
document.documentElement.style.setProperty('--controls-height', `${height}px`)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// CSS variable-based image sizing
|
|
132
|
+
// Simple but effective image sizing function
|
|
150
133
|
const updateImageSizes = useDebounceFn(() => {
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
134
|
+
// Only target main image for sizing
|
|
135
|
+
const mainImage = document.querySelector('.image-display img') as HTMLImageElement
|
|
136
|
+
if (!mainImage) return
|
|
137
|
+
|
|
138
|
+
// Simple, direct calculation of available space
|
|
139
|
+
const topHeight = topControlsRef.value?.offsetHeight || 0
|
|
140
|
+
const infoHeight = infoPanel.value && infoPanelRef.value ? infoPanelRef.value.offsetHeight : 0
|
|
141
|
+
const availableHeight = windowHeight.value - topHeight - infoHeight - 32
|
|
142
|
+
|
|
143
|
+
// Apply size directly to fill available space
|
|
144
|
+
mainImage.style.maxHeight = `${availableHeight}px`
|
|
145
|
+
mainImage.style.width = 'auto'
|
|
146
|
+
|
|
147
|
+
// Handle width constraints
|
|
148
|
+
const sidebarWidth = sidePanel.value ? 256 : 0
|
|
149
|
+
mainImage.style.maxWidth = windowWidth.value <= 768
|
|
150
|
+
? '85vw'
|
|
151
|
+
: `${windowWidth.value - sidebarWidth - 48}px`
|
|
152
|
+
}, 50)
|
|
161
153
|
|
|
162
154
|
// Modal controls
|
|
163
155
|
function setModal(value: boolean) {
|
|
@@ -170,12 +162,7 @@ function setModal(value: boolean) {
|
|
|
170
162
|
useEventListener(document, 'keyup', handleKeyboardRelease)
|
|
171
163
|
}
|
|
172
164
|
|
|
173
|
-
//
|
|
174
|
-
if (windowWidth.value < 1024) {
|
|
175
|
-
controlsTimeout = window.setTimeout(() => {
|
|
176
|
-
showControls.value = false
|
|
177
|
-
}, 3000)
|
|
178
|
-
}
|
|
165
|
+
// No longer auto-hide controls on mobile
|
|
179
166
|
}
|
|
180
167
|
else {
|
|
181
168
|
if (props.onClose) props.onClose()
|
|
@@ -225,6 +212,11 @@ function goNextImage() {
|
|
|
225
212
|
modelValue.value = 0
|
|
226
213
|
}
|
|
227
214
|
resetControlsTimer()
|
|
215
|
+
|
|
216
|
+
// Force image sizing update after navigation
|
|
217
|
+
nextTick(() => {
|
|
218
|
+
updateImageSizes()
|
|
219
|
+
})
|
|
228
220
|
}
|
|
229
221
|
|
|
230
222
|
function goPrevImage() {
|
|
@@ -237,29 +229,22 @@ function goPrevImage() {
|
|
|
237
229
|
modelValue.value = props.images.length - 1 > 0 ? props.images.length - 1 : 0
|
|
238
230
|
}
|
|
239
231
|
resetControlsTimer()
|
|
232
|
+
|
|
233
|
+
// Force image sizing update after navigation
|
|
234
|
+
nextTick(() => {
|
|
235
|
+
updateImageSizes()
|
|
236
|
+
})
|
|
240
237
|
}
|
|
241
238
|
|
|
242
239
|
// UI control functions
|
|
243
240
|
function resetControlsTimer() {
|
|
244
|
-
//
|
|
241
|
+
// Always show controls - no auto-hide
|
|
245
242
|
showControls.value = true
|
|
246
|
-
|
|
247
|
-
// Only set timer on mobile
|
|
248
|
-
if (windowWidth.value < 1024) {
|
|
249
|
-
if (controlsTimeout) {
|
|
250
|
-
clearTimeout(controlsTimeout)
|
|
251
|
-
}
|
|
252
|
-
controlsTimeout = window.setTimeout(() => {
|
|
253
|
-
showControls.value = false
|
|
254
|
-
}, 3000)
|
|
255
|
-
}
|
|
256
243
|
}
|
|
257
244
|
|
|
245
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
258
246
|
function toggleControls() {
|
|
259
247
|
showControls.value = !showControls.value
|
|
260
|
-
if (showControls.value && windowWidth.value < 1024) {
|
|
261
|
-
resetControlsTimer()
|
|
262
|
-
}
|
|
263
248
|
}
|
|
264
249
|
|
|
265
250
|
function toggleInfoPanel() {
|
|
@@ -343,9 +328,8 @@ const touchEnd = useDebounceFn((event: TouchEvent) => {
|
|
|
343
328
|
const diffX = start.x - end.x
|
|
344
329
|
const diffY = start.y - end.y
|
|
345
330
|
|
|
346
|
-
//
|
|
331
|
+
// For taps, we don't toggle controls anymore - they always stay visible
|
|
347
332
|
if (Math.abs(diffX) < 10 && Math.abs(diffY) < 10 && touchDuration < 300) {
|
|
348
|
-
toggleControls()
|
|
349
333
|
return
|
|
350
334
|
}
|
|
351
335
|
|
|
@@ -440,9 +424,6 @@ onMounted(() => {
|
|
|
440
424
|
eventBus.on(`${props.id}Gallery`, openGalleryImage)
|
|
441
425
|
eventBus.on(`${props.id}GalleryClose`, closeGallery)
|
|
442
426
|
|
|
443
|
-
// Initialize CSS variables
|
|
444
|
-
updateImageSizes()
|
|
445
|
-
|
|
446
427
|
// Set up observers for dynamic resizing
|
|
447
428
|
if (topControlsRef.value) {
|
|
448
429
|
useResizeObserver(topControlsRef.value, updateImageSizes)
|
|
@@ -567,7 +548,8 @@ onUnmounted(() => {
|
|
|
567
548
|
>
|
|
568
549
|
<!-- Main Image Area - Fills available space -->
|
|
569
550
|
<div
|
|
570
|
-
class="relative flex-1 h-full flex items-center justify-center
|
|
551
|
+
class="relative flex-1 h-full flex items-center justify-center"
|
|
552
|
+
:style="{ paddingTop: `${topControlsHeight}px` }"
|
|
571
553
|
:class="{ 'lg:pr-64': sidePanel, 'lg:max-w-[calc(100%-16rem)]': sidePanel }"
|
|
572
554
|
style="max-width: 100%;"
|
|
573
555
|
>
|
|
@@ -617,14 +599,15 @@ onUnmounted(() => {
|
|
|
617
599
|
<component
|
|
618
600
|
:is="videoComponent"
|
|
619
601
|
:src="isVideo(images[modelValue])"
|
|
620
|
-
class="shadow max-w-full video-component
|
|
602
|
+
class="shadow max-w-full h-auto object-contain video-component"
|
|
603
|
+
@load="updateImageSizes"
|
|
621
604
|
/>
|
|
622
605
|
</ClientOnly>
|
|
623
606
|
</template>
|
|
624
607
|
<template v-else>
|
|
625
608
|
<img
|
|
626
609
|
v-if="modelValueSrc && imageComponent === 'img'"
|
|
627
|
-
class="shadow max-w-full
|
|
610
|
+
class="shadow max-w-full h-auto object-contain"
|
|
628
611
|
:src="modelValueSrc"
|
|
629
612
|
:alt="`Gallery image ${modelValue + 1}`"
|
|
630
613
|
@load="updateImageSizes"
|
|
@@ -632,11 +615,10 @@ onUnmounted(() => {
|
|
|
632
615
|
<component
|
|
633
616
|
:is="imageComponent"
|
|
634
617
|
v-else-if="modelValueSrc && imageComponent"
|
|
635
|
-
|
|
636
|
-
class="shadow max-w-full gallery-image"
|
|
637
618
|
:image="modelValueSrc.image"
|
|
638
619
|
:variant="modelValueSrc.variant"
|
|
639
620
|
:alt="modelValueSrc.alt"
|
|
621
|
+
class="shadow max-w-full h-auto object-contain"
|
|
640
622
|
/>
|
|
641
623
|
</template>
|
|
642
624
|
</div>
|
|
@@ -680,7 +662,6 @@ onUnmounted(() => {
|
|
|
680
662
|
v-if="infoPanel && images[modelValue]"
|
|
681
663
|
ref="infoPanelRef"
|
|
682
664
|
class="info-panel absolute bottom-0 left-0 right-0 px-4 py-3 backdrop-blur-md bg-fv-neutral-900/70 z-45"
|
|
683
|
-
@transitionend="updateImageSizes"
|
|
684
665
|
>
|
|
685
666
|
<slot :value="images[modelValue]" />
|
|
686
667
|
</div>
|
|
@@ -699,7 +680,8 @@ onUnmounted(() => {
|
|
|
699
680
|
<div
|
|
700
681
|
v-if="sidePanel"
|
|
701
682
|
ref="sidePanelRef"
|
|
702
|
-
class="side-panel hidden lg:block absolute right-0 top-0 bottom-0 w-64 backdrop-blur-md overflow-y-auto z-40 cool-scroll
|
|
683
|
+
class="side-panel hidden lg:block absolute right-0 top-0 bottom-0 w-64 bg-fv-neutral-800/90 backdrop-blur-md overflow-y-auto z-40 cool-scroll"
|
|
684
|
+
:style="{ paddingTop: `${topControlsHeight + 8}px` }"
|
|
703
685
|
>
|
|
704
686
|
<!-- Paging Controls if needed -->
|
|
705
687
|
<div v-if="paging" class="flex items-center justify-center pt-2">
|
|
@@ -901,13 +883,6 @@ onUnmounted(() => {
|
|
|
901
883
|
</template>
|
|
902
884
|
|
|
903
885
|
<style scoped>
|
|
904
|
-
/* CSS variables for dimensions */
|
|
905
|
-
:root {
|
|
906
|
-
--controls-height: 0px;
|
|
907
|
-
--info-height: 0px;
|
|
908
|
-
--sidebar-width: 16rem;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
886
|
/* Ensure controls stay fixed at top */
|
|
912
887
|
.controls-bar {
|
|
913
888
|
height: auto;
|
|
@@ -938,26 +913,6 @@ onUnmounted(() => {
|
|
|
938
913
|
border-top-right-radius: 0.5rem;
|
|
939
914
|
}
|
|
940
915
|
|
|
941
|
-
/* Image sizing in different contexts - simplified approach like old component */
|
|
942
|
-
.gallery-image {
|
|
943
|
-
height: auto;
|
|
944
|
-
object-fit: contain;
|
|
945
|
-
max-width: 92vw;
|
|
946
|
-
max-height: calc(80vh - var(--controls-height) - var(--info-height, 0px));
|
|
947
|
-
transition: max-height 0.3s ease-out, max-width 0.3s ease-out;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
@media (min-width: 1024px) {
|
|
951
|
-
.gallery-image {
|
|
952
|
-
max-width: calc(92vw - var(--sidebar-width) - 48px);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
/* Fullscreen mode sizing */
|
|
957
|
-
:is(.gallery-container[style*="fullscreen"]) .gallery-image {
|
|
958
|
-
max-height: calc(92vh - var(--controls-height) - var(--info-height, 0px));
|
|
959
|
-
}
|
|
960
|
-
|
|
961
916
|
/* Transition styles for next (right) navigation */
|
|
962
917
|
.slide-next-enter-active,
|
|
963
918
|
.slide-next-leave-active {
|
package/package.json
CHANGED
|
@@ -1,1073 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { Component } from 'vue'
|
|
3
|
-
import type { APIPaging } from '../../composables/rest'
|
|
4
|
-
import {
|
|
5
|
-
ChevronDoubleLeftIcon,
|
|
6
|
-
ChevronDoubleRightIcon,
|
|
7
|
-
ChevronLeftIcon,
|
|
8
|
-
ChevronRightIcon,
|
|
9
|
-
InformationCircleIcon,
|
|
10
|
-
XMarkIcon,
|
|
11
|
-
} from '@heroicons/vue/24/solid'
|
|
12
|
-
import { useDebounceFn, useEventListener, useFullscreen, useResizeObserver } from '@vueuse/core'
|
|
13
|
-
import { computed, h, nextTick, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from 'vue'
|
|
14
|
-
import { useEventBus } from '../../composables/event-bus'
|
|
15
|
-
import DefaultPaging from './DefaultPaging.vue'
|
|
16
|
-
|
|
17
|
-
const isGalleryOpen = ref<boolean>(false)
|
|
18
|
-
const eventBus = useEventBus()
|
|
19
|
-
const sidePanel = ref<boolean>(true)
|
|
20
|
-
const showControls = ref<boolean>(true)
|
|
21
|
-
const isFullscreen = ref<boolean>(false)
|
|
22
|
-
const infoPanel = ref<boolean>(true) // Show info panel by default
|
|
23
|
-
const touchStartTime = ref<number>(0)
|
|
24
|
-
const infoHeight = ref<number>(0)
|
|
25
|
-
const galleryContainerRef = shallowRef<HTMLElement | null>(null)
|
|
26
|
-
|
|
27
|
-
// Use VueUse's useFullscreen for better fullscreen handling
|
|
28
|
-
const { isFullscreen: isElementFullscreen, enter: enterFullscreen, exit: exitFullscreen } = useFullscreen(galleryContainerRef)
|
|
29
|
-
|
|
30
|
-
// Track when fullscreen changes externally (like Escape key)
|
|
31
|
-
watch(isElementFullscreen, (newValue) => {
|
|
32
|
-
isFullscreen.value = newValue
|
|
33
|
-
if (newValue) {
|
|
34
|
-
// Force update of image size when entering fullscreen
|
|
35
|
-
nextTick(() => {
|
|
36
|
-
updateImageSizes()
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
const props = withDefaults(
|
|
42
|
-
defineProps<{
|
|
43
|
-
id: string
|
|
44
|
-
images: Array<any>
|
|
45
|
-
title?: string
|
|
46
|
-
getImageUrl?: Function
|
|
47
|
-
getThumbnailUrl?: Function
|
|
48
|
-
onOpen?: Function
|
|
49
|
-
onClose?: Function
|
|
50
|
-
closeIcon?: object
|
|
51
|
-
gridHeight?: number
|
|
52
|
-
mode: 'mason' | 'grid' | 'button' | 'hidden' | 'custom'
|
|
53
|
-
paging?: APIPaging | undefined
|
|
54
|
-
buttonText?: string
|
|
55
|
-
buttonType?: string
|
|
56
|
-
modelValue: number
|
|
57
|
-
borderColor?: Function
|
|
58
|
-
imageLoader: string
|
|
59
|
-
videoComponent?: Component | string
|
|
60
|
-
imageComponent?: Component | string
|
|
61
|
-
isVideo?: Function
|
|
62
|
-
ranking?: boolean
|
|
63
|
-
}>(),
|
|
64
|
-
{
|
|
65
|
-
modelValue: 0,
|
|
66
|
-
imageComponent: 'img',
|
|
67
|
-
mode: 'grid',
|
|
68
|
-
gridHeight: 4,
|
|
69
|
-
closeIcon: () => h(XMarkIcon),
|
|
70
|
-
images: () => [],
|
|
71
|
-
isVideo: () => false,
|
|
72
|
-
getImageUrl: (image: any) => image.image_url,
|
|
73
|
-
getThumbnailUrl: (image: any) => `${image.image_url}?s=250x250&m=autocrop`,
|
|
74
|
-
paging: undefined,
|
|
75
|
-
borderColor: undefined,
|
|
76
|
-
ranking: false,
|
|
77
|
-
},
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
const emit = defineEmits(['update:modelValue'])
|
|
81
|
-
const modelValue = computed({
|
|
82
|
-
get: () => props.modelValue,
|
|
83
|
-
set: (i) => {
|
|
84
|
-
emit('update:modelValue', i)
|
|
85
|
-
},
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
const direction = ref<'next' | 'prev'>('next')
|
|
89
|
-
|
|
90
|
-
let controlsTimeout: number | null = null
|
|
91
|
-
let fullscreenResizeTimeout: number | null = null
|
|
92
|
-
|
|
93
|
-
// Used to maintain consistent image sizes
|
|
94
|
-
function updateImageSizes() {
|
|
95
|
-
nextTick(() => {
|
|
96
|
-
const imageContainers = document.querySelectorAll('.image-container img, .image-container .video-component') as NodeListOf<HTMLElement>
|
|
97
|
-
if (imageContainers && imageContainers.length > 0) {
|
|
98
|
-
imageContainers.forEach((img) => {
|
|
99
|
-
// Force a reflow to ensure correct sizing
|
|
100
|
-
img.style.maxHeight = ''
|
|
101
|
-
// Force browser to recalculate styles
|
|
102
|
-
void img.offsetHeight
|
|
103
|
-
|
|
104
|
-
// Calculate the correct height based on fullscreen and info panel state
|
|
105
|
-
const topHeight = 4 // rem
|
|
106
|
-
const infoHeight = infoPanel.value ? Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--info-height') || '0px') / 16 : 0
|
|
107
|
-
const viewHeight = isFullscreen.value ? 90 : 80
|
|
108
|
-
|
|
109
|
-
// Set explicit height with important to override any other styles
|
|
110
|
-
img.style.maxHeight = `calc(${viewHeight}vh - ${infoHeight}rem - ${topHeight}rem) !important`
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function setModal(value: boolean) {
|
|
117
|
-
if (value === true) {
|
|
118
|
-
if (props.onOpen) props.onOpen()
|
|
119
|
-
document.body.style.overflow = 'hidden' // Prevent scrolling when gallery is open
|
|
120
|
-
if (!import.meta.env.SSR) {
|
|
121
|
-
useEventListener(document, 'keydown', handleKeyboardInput)
|
|
122
|
-
useEventListener(document, 'keyup', handleKeyboardRelease)
|
|
123
|
-
}
|
|
124
|
-
// Auto-hide controls after 3 seconds on mobile
|
|
125
|
-
if (window.innerWidth < 1024) {
|
|
126
|
-
controlsTimeout = window.setTimeout(() => {
|
|
127
|
-
showControls.value = false
|
|
128
|
-
}, 3000)
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
if (props.onClose) props.onClose()
|
|
133
|
-
document.body.style.overflow = '' // Restore scrolling
|
|
134
|
-
// Exit fullscreen if active
|
|
135
|
-
if (isFullscreen.value) {
|
|
136
|
-
exitFullscreen()
|
|
137
|
-
isFullscreen.value = false
|
|
138
|
-
}
|
|
139
|
-
// Clear timeout if modal is closed
|
|
140
|
-
if (controlsTimeout) {
|
|
141
|
-
clearTimeout(controlsTimeout)
|
|
142
|
-
controlsTimeout = null
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
isGalleryOpen.value = value
|
|
146
|
-
showControls.value = true
|
|
147
|
-
// Don't reset info panel state when opening/closing
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const openGalleryImage = useDebounceFn((index: number | undefined) => {
|
|
151
|
-
if (index === undefined) {
|
|
152
|
-
modelValue.value = 0
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
modelValue.value = Number.parseInt(index.toString())
|
|
156
|
-
}
|
|
157
|
-
setModal(true)
|
|
158
|
-
}, 50) // Debounce to prevent accidental double-opens
|
|
159
|
-
|
|
160
|
-
function goNextImage() {
|
|
161
|
-
direction.value = 'next'
|
|
162
|
-
if (modelValue.value < props.images.length - 1) {
|
|
163
|
-
modelValue.value++
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
modelValue.value = 0
|
|
167
|
-
}
|
|
168
|
-
resetControlsTimer()
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function goPrevImage() {
|
|
172
|
-
direction.value = 'prev'
|
|
173
|
-
if (modelValue.value > 0) {
|
|
174
|
-
modelValue.value--
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
modelValue.value
|
|
178
|
-
= props.images.length - 1 > 0 ? props.images.length - 1 : 0
|
|
179
|
-
}
|
|
180
|
-
resetControlsTimer()
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const modelValueSrc = computed(() => {
|
|
184
|
-
if (props.images.length === 0) return false
|
|
185
|
-
if (props.images[modelValue.value] === undefined) return false
|
|
186
|
-
return props.getImageUrl(props.images[modelValue.value])
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
const currentImage = computed(() => {
|
|
190
|
-
if (props.images.length === 0) return null
|
|
191
|
-
return props.images[modelValue.value]
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
const imageCount = computed(() => props.images.length)
|
|
195
|
-
const currentIndex = computed(() => modelValue.value + 1)
|
|
196
|
-
|
|
197
|
-
const start = reactive({ x: 0, y: 0 })
|
|
198
|
-
|
|
199
|
-
function resetControlsTimer() {
|
|
200
|
-
// Show controls when user interacts
|
|
201
|
-
showControls.value = true
|
|
202
|
-
|
|
203
|
-
// Only set timer on mobile
|
|
204
|
-
if (window.innerWidth < 1024) {
|
|
205
|
-
if (controlsTimeout) {
|
|
206
|
-
clearTimeout(controlsTimeout)
|
|
207
|
-
}
|
|
208
|
-
controlsTimeout = window.setTimeout(() => {
|
|
209
|
-
showControls.value = false
|
|
210
|
-
}, 3000)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function toggleControls() {
|
|
215
|
-
showControls.value = !showControls.value
|
|
216
|
-
if (showControls.value && window.innerWidth < 1024) {
|
|
217
|
-
resetControlsTimer()
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function toggleInfoPanel() {
|
|
222
|
-
infoPanel.value = !infoPanel.value
|
|
223
|
-
resetControlsTimer()
|
|
224
|
-
|
|
225
|
-
// Update the info height after panel toggle
|
|
226
|
-
if (infoPanel.value) {
|
|
227
|
-
nextTick(() => {
|
|
228
|
-
updateInfoHeight()
|
|
229
|
-
})
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
// Reset when hiding
|
|
233
|
-
document.documentElement.style.setProperty('--info-height', '0px')
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function toggleFullscreen() {
|
|
238
|
-
if (!isFullscreen.value) {
|
|
239
|
-
if (galleryContainerRef.value) {
|
|
240
|
-
enterFullscreen()
|
|
241
|
-
.then(() => {
|
|
242
|
-
isFullscreen.value = true
|
|
243
|
-
// Give browser time to adjust fullscreen before updating sizing
|
|
244
|
-
if (fullscreenResizeTimeout) clearTimeout(fullscreenResizeTimeout)
|
|
245
|
-
fullscreenResizeTimeout = window.setTimeout(() => {
|
|
246
|
-
updateImageSizes()
|
|
247
|
-
}, 50)
|
|
248
|
-
})
|
|
249
|
-
.catch(() => {})
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
exitFullscreen()
|
|
254
|
-
.then(() => {
|
|
255
|
-
isFullscreen.value = false
|
|
256
|
-
})
|
|
257
|
-
.catch(() => {})
|
|
258
|
-
}
|
|
259
|
-
resetControlsTimer()
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Touch handling with debounce to prevent multiple rapid changes
|
|
263
|
-
const touchStart = useDebounceFn((event: TouchEvent) => {
|
|
264
|
-
const touch = event.touches[0]
|
|
265
|
-
const targetElement = touch.target as HTMLElement
|
|
266
|
-
|
|
267
|
-
// Store start time for tap detection
|
|
268
|
-
touchStartTime.value = Date.now()
|
|
269
|
-
|
|
270
|
-
// Check if the touch started on an interactive element
|
|
271
|
-
if (targetElement.closest('button, a, input, textarea, select')) {
|
|
272
|
-
return // Don't handle swipe if interacting with an interactive element
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
start.x = touch.screenX
|
|
276
|
-
start.y = touch.screenY
|
|
277
|
-
}, 50)
|
|
278
|
-
|
|
279
|
-
const touchEnd = useDebounceFn((event: TouchEvent) => {
|
|
280
|
-
const touch = event.changedTouches[0]
|
|
281
|
-
const targetElement = touch.target as HTMLElement
|
|
282
|
-
const touchDuration = Date.now() - touchStartTime.value
|
|
283
|
-
|
|
284
|
-
// Check if the touch ended on an interactive element
|
|
285
|
-
if (targetElement.closest('button, a, input, textarea, select')) {
|
|
286
|
-
return // Don't handle swipe if interacting with an interactive element
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const end = { x: touch.screenX, y: touch.screenY }
|
|
290
|
-
|
|
291
|
-
const diffX = start.x - end.x
|
|
292
|
-
const diffY = start.y - end.y
|
|
293
|
-
|
|
294
|
-
// Detect tap (quick touch with minimal movement)
|
|
295
|
-
if (Math.abs(diffX) < 10 && Math.abs(diffY) < 10 && touchDuration < 300) {
|
|
296
|
-
toggleControls()
|
|
297
|
-
return
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Add a threshold to prevent accidental swipes
|
|
301
|
-
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
|
|
302
|
-
if (diffX > 0) {
|
|
303
|
-
direction.value = 'next'
|
|
304
|
-
goNextImage()
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
direction.value = 'prev'
|
|
308
|
-
goPrevImage()
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}, 50)
|
|
312
|
-
|
|
313
|
-
function getBorderColor(i: any) {
|
|
314
|
-
if (props.borderColor !== undefined) {
|
|
315
|
-
return props.borderColor(i)
|
|
316
|
-
}
|
|
317
|
-
return ''
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const isKeyPressed = ref<boolean>(false)
|
|
321
|
-
|
|
322
|
-
function handleKeyboardInput(event: KeyboardEvent) {
|
|
323
|
-
if (!isGalleryOpen.value) return
|
|
324
|
-
if (isKeyPressed.value) return
|
|
325
|
-
|
|
326
|
-
switch (event.key) {
|
|
327
|
-
case 'Escape':
|
|
328
|
-
event.preventDefault()
|
|
329
|
-
setModal(false)
|
|
330
|
-
break
|
|
331
|
-
case 'ArrowRight':
|
|
332
|
-
isKeyPressed.value = true
|
|
333
|
-
direction.value = 'next'
|
|
334
|
-
goNextImage()
|
|
335
|
-
break
|
|
336
|
-
case 'ArrowLeft':
|
|
337
|
-
isKeyPressed.value = true
|
|
338
|
-
direction.value = 'prev'
|
|
339
|
-
goPrevImage()
|
|
340
|
-
break
|
|
341
|
-
case 'f':
|
|
342
|
-
toggleFullscreen()
|
|
343
|
-
break
|
|
344
|
-
case 'i':
|
|
345
|
-
toggleInfoPanel()
|
|
346
|
-
break
|
|
347
|
-
default:
|
|
348
|
-
break
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function handleKeyboardRelease(event: KeyboardEvent) {
|
|
353
|
-
if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
|
|
354
|
-
isKeyPressed.value = false
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function closeGallery() {
|
|
359
|
-
setModal(false)
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Click outside gallery content to close - with debounce to prevent accidental closes
|
|
363
|
-
const handleBackdropClick = useDebounceFn((event: MouseEvent) => {
|
|
364
|
-
if (event.target === event.currentTarget) {
|
|
365
|
-
setModal(false)
|
|
366
|
-
}
|
|
367
|
-
}, 200)
|
|
368
|
-
|
|
369
|
-
// Watch for both image changes and fullscreen mode changes
|
|
370
|
-
watch([currentImage, isFullscreen], () => {
|
|
371
|
-
// Update the info height when image changes or fullscreen state changes
|
|
372
|
-
if (infoPanel.value) {
|
|
373
|
-
nextTick(() => {
|
|
374
|
-
updateInfoHeight()
|
|
375
|
-
// Fix image sizing issue when navigating in fullscreen
|
|
376
|
-
if (isFullscreen.value) {
|
|
377
|
-
updateImageSizes()
|
|
378
|
-
}
|
|
379
|
-
})
|
|
380
|
-
}
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
// Update CSS variable with info panel height
|
|
384
|
-
function updateInfoHeight() {
|
|
385
|
-
nextTick(() => {
|
|
386
|
-
const infoElement = document.querySelector('.info-panel-slot') as HTMLElement
|
|
387
|
-
if (infoElement) {
|
|
388
|
-
const height = infoElement.offsetHeight
|
|
389
|
-
infoHeight.value = height
|
|
390
|
-
document.documentElement.style.setProperty('--info-height', `${height}px`)
|
|
391
|
-
}
|
|
392
|
-
})
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Debounced window resize handler to avoid performance issues
|
|
396
|
-
const handleWindowResize = useDebounceFn(() => {
|
|
397
|
-
if (isGalleryOpen.value) {
|
|
398
|
-
updateImageSizes()
|
|
399
|
-
updateInfoHeight()
|
|
400
|
-
}
|
|
401
|
-
}, 100)
|
|
402
|
-
|
|
403
|
-
onMounted(() => {
|
|
404
|
-
eventBus.on(`${props.id}GalleryImage`, openGalleryImage)
|
|
405
|
-
eventBus.on(`${props.id}Gallery`, openGalleryImage)
|
|
406
|
-
eventBus.on(`${props.id}GalleryClose`, closeGallery)
|
|
407
|
-
|
|
408
|
-
// Store reference to the gallery container
|
|
409
|
-
galleryContainerRef.value = document.querySelector('.gallery-container')
|
|
410
|
-
|
|
411
|
-
// Initialize info height once mounted (only if info panel is shown)
|
|
412
|
-
if (infoPanel.value) {
|
|
413
|
-
updateInfoHeight()
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Use vueUse's useResizeObserver instead of native ResizeObserver
|
|
417
|
-
const infoElement = document.querySelector('.info-panel-slot')
|
|
418
|
-
if (infoElement) {
|
|
419
|
-
useResizeObserver(infoElement as HTMLElement, () => {
|
|
420
|
-
if (infoPanel.value) {
|
|
421
|
-
updateInfoHeight()
|
|
422
|
-
}
|
|
423
|
-
})
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// Listen for window resize events
|
|
427
|
-
useEventListener(window, 'resize', handleWindowResize)
|
|
428
|
-
|
|
429
|
-
// Listen for fullscreen changes to update image sizes
|
|
430
|
-
useEventListener(document, 'fullscreenchange', () => {
|
|
431
|
-
if (document.fullscreenElement) {
|
|
432
|
-
// This handles the case of using F11 or browser fullscreen controls
|
|
433
|
-
isFullscreen.value = true
|
|
434
|
-
updateImageSizes()
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
isFullscreen.value = false
|
|
438
|
-
}
|
|
439
|
-
})
|
|
440
|
-
})
|
|
441
|
-
|
|
442
|
-
onUnmounted(() => {
|
|
443
|
-
eventBus.off(`${props.id}Gallery`, openGalleryImage)
|
|
444
|
-
eventBus.off(`${props.id}GalleryImage`, openGalleryImage)
|
|
445
|
-
eventBus.off(`${props.id}GalleryClose`, closeGallery)
|
|
446
|
-
|
|
447
|
-
if (!import.meta.env.SSR) {
|
|
448
|
-
document.body.style.overflow = '' // Ensure body scrolling is restored
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Clear any remaining timeouts
|
|
452
|
-
if (controlsTimeout) {
|
|
453
|
-
clearTimeout(controlsTimeout)
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (fullscreenResizeTimeout) {
|
|
457
|
-
clearTimeout(fullscreenResizeTimeout)
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Ensure we exit fullscreen mode on unmount
|
|
461
|
-
if (isFullscreen.value) {
|
|
462
|
-
exitFullscreen().catch(() => {})
|
|
463
|
-
}
|
|
464
|
-
})
|
|
465
|
-
</script>
|
|
466
|
-
|
|
467
|
-
<template>
|
|
468
|
-
<div>
|
|
469
|
-
<transition
|
|
470
|
-
enter-active-class="duration-300 ease-out"
|
|
471
|
-
enter-from-class="opacity-0"
|
|
472
|
-
enter-to-class="opacity-100"
|
|
473
|
-
leave-active-class="duration-200 ease-in"
|
|
474
|
-
leave-from-class="opacity-100"
|
|
475
|
-
leave-to-class="opacity-0"
|
|
476
|
-
>
|
|
477
|
-
<div
|
|
478
|
-
v-if="isGalleryOpen"
|
|
479
|
-
class="fixed bg-fv-neutral-900 text-white inset-0 max-w-[100vw] overflow-hidden gallery-container"
|
|
480
|
-
style="z-index: 37"
|
|
481
|
-
role="dialog"
|
|
482
|
-
aria-modal="true"
|
|
483
|
-
@click="handleBackdropClick"
|
|
484
|
-
>
|
|
485
|
-
<div
|
|
486
|
-
class="relative w-full h-full max-w-full flex flex-col justify-center items-center"
|
|
487
|
-
style="z-index: 38"
|
|
488
|
-
@click.stop
|
|
489
|
-
>
|
|
490
|
-
<!-- Main Content Area -->
|
|
491
|
-
<div class="flex flex-grow gap-4 w-full h-full max-w-full">
|
|
492
|
-
<div class="flex-grow h-full flex items-center relative">
|
|
493
|
-
<!-- Image Display Area -->
|
|
494
|
-
<div
|
|
495
|
-
class="flex h-full relative flex-grow items-center justify-center gap-2 z-[1]"
|
|
496
|
-
@touchstart="touchStart"
|
|
497
|
-
@touchend="touchEnd"
|
|
498
|
-
>
|
|
499
|
-
<!-- Image Navigation - Left -->
|
|
500
|
-
<transition
|
|
501
|
-
enter-active-class="transition-opacity duration-300"
|
|
502
|
-
enter-from-class="opacity-0"
|
|
503
|
-
enter-to-class="opacity-100"
|
|
504
|
-
leave-active-class="transition-opacity duration-300"
|
|
505
|
-
leave-from-class="opacity-100"
|
|
506
|
-
leave-to-class="opacity-0"
|
|
507
|
-
>
|
|
508
|
-
<div
|
|
509
|
-
v-if="showControls && images.length > 1"
|
|
510
|
-
class="absolute left-0 z-[40] h-full flex items-center px-2 md:px-4"
|
|
511
|
-
>
|
|
512
|
-
<button
|
|
513
|
-
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"
|
|
514
|
-
aria-label="Previous image"
|
|
515
|
-
@click="goPrevImage()"
|
|
516
|
-
>
|
|
517
|
-
<ChevronLeftIcon class="w-6 h-6 md:w-8 md:h-8" />
|
|
518
|
-
</button>
|
|
519
|
-
</div>
|
|
520
|
-
</transition>
|
|
521
|
-
|
|
522
|
-
<!-- Main Image Container -->
|
|
523
|
-
<div
|
|
524
|
-
class="flex-1 flex flex-col z-[2] items-center justify-center max-w-full lg:max-w-[calc(100vw - 256px)] relative"
|
|
525
|
-
style="padding-top: 1rem;"
|
|
526
|
-
>
|
|
527
|
-
<transition
|
|
528
|
-
:name="direction === 'next' ? 'slide-next' : 'slide-prev'"
|
|
529
|
-
mode="out-in"
|
|
530
|
-
>
|
|
531
|
-
<div
|
|
532
|
-
:key="`image-display-${modelValue}`"
|
|
533
|
-
class="flex-1 w-full max-w-full flex flex-col items-center justify-center absolute inset-0 z-[2]"
|
|
534
|
-
>
|
|
535
|
-
<div
|
|
536
|
-
class="flex-1 w-full max-w-full flex items-center justify-center image-container"
|
|
537
|
-
>
|
|
538
|
-
<template
|
|
539
|
-
v-if="videoComponent && isVideo(images[modelValue])"
|
|
540
|
-
>
|
|
541
|
-
<ClientOnly>
|
|
542
|
-
<component
|
|
543
|
-
:is="videoComponent"
|
|
544
|
-
:src="isVideo(images[modelValue])"
|
|
545
|
-
class="shadow max-w-full h-auto object-contain video-component"
|
|
546
|
-
:style="{
|
|
547
|
-
maxHeight: isFullscreen
|
|
548
|
-
? infoPanel
|
|
549
|
-
? 'calc(90vh - var(--info-height, 0px) - 4rem)'
|
|
550
|
-
: 'calc(90vh - 4rem)'
|
|
551
|
-
: infoPanel
|
|
552
|
-
? 'calc(80vh - var(--info-height, 0px) - 4rem)'
|
|
553
|
-
: 'calc(80vh - 4rem)',
|
|
554
|
-
}"
|
|
555
|
-
/>
|
|
556
|
-
</ClientOnly>
|
|
557
|
-
</template>
|
|
558
|
-
<template v-else>
|
|
559
|
-
<img
|
|
560
|
-
v-if="modelValueSrc && imageComponent === 'img'"
|
|
561
|
-
class="shadow max-w-full h-auto object-contain"
|
|
562
|
-
:style="{
|
|
563
|
-
maxHeight: isFullscreen
|
|
564
|
-
? infoPanel
|
|
565
|
-
? 'calc(90vh - var(--info-height, 0px) - 4rem)'
|
|
566
|
-
: 'calc(90vh - 4rem)'
|
|
567
|
-
: infoPanel
|
|
568
|
-
? 'calc(80vh - var(--info-height, 0px) - 4rem)'
|
|
569
|
-
: 'calc(80vh - 4rem)',
|
|
570
|
-
}"
|
|
571
|
-
:src="modelValueSrc"
|
|
572
|
-
:alt="`Gallery image ${modelValue + 1}`"
|
|
573
|
-
>
|
|
574
|
-
<component
|
|
575
|
-
:is="imageComponent"
|
|
576
|
-
v-else-if="modelValueSrc && imageComponent"
|
|
577
|
-
:image="modelValueSrc.image"
|
|
578
|
-
:variant="modelValueSrc.variant"
|
|
579
|
-
:alt="modelValueSrc.alt"
|
|
580
|
-
class="shadow max-w-full h-auto object-contain"
|
|
581
|
-
:style="{
|
|
582
|
-
maxHeight: isFullscreen
|
|
583
|
-
? infoPanel
|
|
584
|
-
? 'calc(90vh - var(--info-height, 0px) - 4rem)'
|
|
585
|
-
: 'calc(90vh - 4rem)'
|
|
586
|
-
: infoPanel
|
|
587
|
-
? 'calc(80vh - var(--info-height, 0px) - 4rem)'
|
|
588
|
-
: 'calc(80vh - 4rem)',
|
|
589
|
-
}"
|
|
590
|
-
/>
|
|
591
|
-
</template>
|
|
592
|
-
</div>
|
|
593
|
-
|
|
594
|
-
<!-- Image Slot Content -->
|
|
595
|
-
<div
|
|
596
|
-
v-if="infoPanel"
|
|
597
|
-
class="info-panel-slot 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"
|
|
598
|
-
@transitionend="updateInfoHeight"
|
|
599
|
-
>
|
|
600
|
-
<slot :value="images[modelValue]" />
|
|
601
|
-
</div>
|
|
602
|
-
</div>
|
|
603
|
-
</transition>
|
|
604
|
-
</div>
|
|
605
|
-
|
|
606
|
-
<!-- Image Navigation - Right -->
|
|
607
|
-
<transition
|
|
608
|
-
enter-active-class="transition-opacity duration-300"
|
|
609
|
-
enter-from-class="opacity-0"
|
|
610
|
-
enter-to-class="opacity-100"
|
|
611
|
-
leave-active-class="transition-opacity duration-300"
|
|
612
|
-
leave-from-class="opacity-100"
|
|
613
|
-
leave-to-class="opacity-0"
|
|
614
|
-
>
|
|
615
|
-
<div
|
|
616
|
-
v-if="showControls && images.length > 1"
|
|
617
|
-
class="absolute right-0 z-[40] h-full flex items-center px-2 md:px-4"
|
|
618
|
-
>
|
|
619
|
-
<button
|
|
620
|
-
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"
|
|
621
|
-
aria-label="Next image"
|
|
622
|
-
@click="goNextImage()"
|
|
623
|
-
>
|
|
624
|
-
<ChevronRightIcon class="w-6 h-6 md:w-8 md:h-8" />
|
|
625
|
-
</button>
|
|
626
|
-
</div>
|
|
627
|
-
</transition>
|
|
628
|
-
</div>
|
|
629
|
-
</div>
|
|
630
|
-
|
|
631
|
-
<!-- Side Panel for Thumbnails -->
|
|
632
|
-
<transition
|
|
633
|
-
enter-active-class="transform transition ease-in-out duration-300"
|
|
634
|
-
enter-from-class="translate-x-full"
|
|
635
|
-
enter-to-class="translate-x-0"
|
|
636
|
-
leave-active-class="transform transition ease-in-out duration-300"
|
|
637
|
-
leave-from-class="translate-x-0"
|
|
638
|
-
leave-to-class="translate-x-full"
|
|
639
|
-
>
|
|
640
|
-
<div
|
|
641
|
-
v-if="sidePanel"
|
|
642
|
-
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"
|
|
643
|
-
style="padding-top: 4rem;"
|
|
644
|
-
>
|
|
645
|
-
<!-- Paging Controls -->
|
|
646
|
-
<div v-if="paging" class="flex items-center justify-center pt-2">
|
|
647
|
-
<DefaultPaging :id="id" :items="paging" />
|
|
648
|
-
</div>
|
|
649
|
-
|
|
650
|
-
<!-- Thumbnail Grid -->
|
|
651
|
-
<div class="grid grid-cols-2 gap-2 p-2">
|
|
652
|
-
<div
|
|
653
|
-
v-for="i in images.length"
|
|
654
|
-
:key="`bg_${id}_${i}`"
|
|
655
|
-
class="group relative"
|
|
656
|
-
>
|
|
657
|
-
<div
|
|
658
|
-
class="absolute inset-0 rounded-lg transition-colors duration-300 group-hover:bg-fv-neutral-700/40"
|
|
659
|
-
:class="{ 'bg-fv-primary-500/40': i - 1 === modelValue }"
|
|
660
|
-
/>
|
|
661
|
-
<img
|
|
662
|
-
v-if="imageComponent === 'img'"
|
|
663
|
-
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow transition-all duration-300 group-hover:brightness-110 ${getBorderColor(
|
|
664
|
-
images[i - 1],
|
|
665
|
-
)}`"
|
|
666
|
-
:style="{
|
|
667
|
-
filter:
|
|
668
|
-
i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
669
|
-
}"
|
|
670
|
-
:src="getThumbnailUrl(images[i - 1])"
|
|
671
|
-
:alt="`Thumbnail ${i}`"
|
|
672
|
-
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
673
|
-
>
|
|
674
|
-
<component
|
|
675
|
-
:is="imageComponent"
|
|
676
|
-
v-else
|
|
677
|
-
:image="getThumbnailUrl(images[i - 1]).image"
|
|
678
|
-
:variant="getThumbnailUrl(images[i - 1]).variant"
|
|
679
|
-
:alt="getThumbnailUrl(images[i - 1]).alt"
|
|
680
|
-
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow transition-all duration-300 group-hover:brightness-110 ${getBorderColor(
|
|
681
|
-
images[i - 1],
|
|
682
|
-
)}`"
|
|
683
|
-
:style="{
|
|
684
|
-
filter:
|
|
685
|
-
i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
686
|
-
}"
|
|
687
|
-
:likes="getThumbnailUrl(images[i - 1]).likes"
|
|
688
|
-
:show-likes="getThumbnailUrl(images[i - 1]).showLikes"
|
|
689
|
-
:is-author="getThumbnailUrl(images[i - 1]).isAuthor"
|
|
690
|
-
:user-uuid="getThumbnailUrl(images[i - 1]).userUUID"
|
|
691
|
-
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
692
|
-
/>
|
|
693
|
-
</div>
|
|
694
|
-
</div>
|
|
695
|
-
</div>
|
|
696
|
-
</transition>
|
|
697
|
-
</div>
|
|
698
|
-
|
|
699
|
-
<!-- Top Controls -->
|
|
700
|
-
<transition
|
|
701
|
-
enter-active-class="transition-opacity duration-300"
|
|
702
|
-
enter-from-class="opacity-0"
|
|
703
|
-
enter-to-class="opacity-100"
|
|
704
|
-
leave-active-class="transition-opacity duration-300"
|
|
705
|
-
leave-from-class="opacity-100"
|
|
706
|
-
leave-to-class="opacity-0"
|
|
707
|
-
>
|
|
708
|
-
<div
|
|
709
|
-
v-if="showControls"
|
|
710
|
-
class="fixed 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 h-16"
|
|
711
|
-
>
|
|
712
|
-
<!-- Title and Counter -->
|
|
713
|
-
<div class="flex items-center space-x-2">
|
|
714
|
-
<span v-if="title" class="font-medium text-lg">{{ title }}</span>
|
|
715
|
-
<span class="text-sm opacity-80">{{ currentIndex }} / {{ imageCount }}</span>
|
|
716
|
-
</div>
|
|
717
|
-
|
|
718
|
-
<!-- Control Buttons -->
|
|
719
|
-
<div class="flex items-center space-x-2">
|
|
720
|
-
<button
|
|
721
|
-
class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
722
|
-
:class="{ 'bg-fv-primary-500/70': infoPanel }"
|
|
723
|
-
:title="infoPanel ? 'Hide info' : 'Show info'"
|
|
724
|
-
@click="toggleInfoPanel"
|
|
725
|
-
>
|
|
726
|
-
<InformationCircleIcon class="w-5 h-5" />
|
|
727
|
-
</button>
|
|
728
|
-
|
|
729
|
-
<button
|
|
730
|
-
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"
|
|
731
|
-
:title="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
|
|
732
|
-
@click="() => (sidePanel = !sidePanel)"
|
|
733
|
-
>
|
|
734
|
-
<ChevronDoubleRightIcon v-if="sidePanel" class="w-5 h-5" />
|
|
735
|
-
<ChevronDoubleLeftIcon v-else class="w-5 h-5" />
|
|
736
|
-
</button>
|
|
737
|
-
|
|
738
|
-
<button
|
|
739
|
-
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"
|
|
740
|
-
:title="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
|
|
741
|
-
@click="() => (sidePanel = !sidePanel)"
|
|
742
|
-
>
|
|
743
|
-
<ChevronDoubleRightIcon v-if="sidePanel" class="w-5 h-5" />
|
|
744
|
-
<ChevronDoubleLeftIcon v-else class="w-5 h-5" />
|
|
745
|
-
</button>
|
|
746
|
-
|
|
747
|
-
<button
|
|
748
|
-
class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
749
|
-
aria-label="Close gallery"
|
|
750
|
-
@click="setModal(false)"
|
|
751
|
-
>
|
|
752
|
-
<component :is="closeIcon" class="w-5 h-5" />
|
|
753
|
-
</button>
|
|
754
|
-
</div>
|
|
755
|
-
</div>
|
|
756
|
-
</transition>
|
|
757
|
-
|
|
758
|
-
<!-- Mobile Thumbnail Preview -->
|
|
759
|
-
<transition
|
|
760
|
-
enter-active-class="transition-transform duration-300 ease-out"
|
|
761
|
-
enter-from-class="translate-y-full"
|
|
762
|
-
enter-to-class="translate-y-0"
|
|
763
|
-
leave-active-class="transition-transform duration-300 ease-in"
|
|
764
|
-
leave-from-class="translate-y-0"
|
|
765
|
-
leave-to-class="translate-y-full"
|
|
766
|
-
>
|
|
767
|
-
<div
|
|
768
|
-
v-if="showControls && images.length > 1 && !sidePanel"
|
|
769
|
-
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]"
|
|
770
|
-
>
|
|
771
|
-
<div class="overflow-x-auto flex space-x-2 pb-1 px-1">
|
|
772
|
-
<div
|
|
773
|
-
v-for="(image, idx) in images"
|
|
774
|
-
:key="`mobile_thumb_${id}_${idx}`"
|
|
775
|
-
class="flex-shrink-0 w-16 h-16 rounded-lg relative cursor-pointer"
|
|
776
|
-
:class="{ 'ring-2 ring-fv-primary-500 ring-offset-1 ring-offset-fv-neutral-900': idx === modelValue }"
|
|
777
|
-
@click="$eventBus.emit(`${id}GalleryImage`, idx)"
|
|
778
|
-
>
|
|
779
|
-
<img
|
|
780
|
-
v-if="imageComponent === 'img'"
|
|
781
|
-
class="w-full h-full object-cover rounded-lg transition duration-200"
|
|
782
|
-
:style="{
|
|
783
|
-
filter: idx === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
784
|
-
}"
|
|
785
|
-
:src="getThumbnailUrl(image)"
|
|
786
|
-
:alt="`Thumbnail ${idx + 1}`"
|
|
787
|
-
>
|
|
788
|
-
<component
|
|
789
|
-
:is="imageComponent"
|
|
790
|
-
v-else
|
|
791
|
-
:image="getThumbnailUrl(image).image"
|
|
792
|
-
:variant="getThumbnailUrl(image).variant"
|
|
793
|
-
:alt="getThumbnailUrl(image).alt"
|
|
794
|
-
class="w-full h-full object-cover rounded-lg transition duration-200"
|
|
795
|
-
:style="{
|
|
796
|
-
filter: idx === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
797
|
-
}"
|
|
798
|
-
/>
|
|
799
|
-
</div>
|
|
800
|
-
</div>
|
|
801
|
-
</div>
|
|
802
|
-
</transition>
|
|
803
|
-
</div>
|
|
804
|
-
</div>
|
|
805
|
-
</transition>
|
|
806
|
-
|
|
807
|
-
<!-- Thumbnail Grid/Mason/Custom Layouts -->
|
|
808
|
-
<div v-if="mode === 'grid' || mode === 'mason' || mode === 'custom'" class="gallery-grid">
|
|
809
|
-
<div
|
|
810
|
-
:class="{
|
|
811
|
-
'masonry-grid': mode === 'mason',
|
|
812
|
-
'standard-grid': mode === 'grid',
|
|
813
|
-
'custom-grid': mode === 'custom',
|
|
814
|
-
}"
|
|
815
|
-
>
|
|
816
|
-
<slot name="thumbnail" />
|
|
817
|
-
<template v-for="i in images.length" :key="`g_${id}_${i}`">
|
|
818
|
-
<template v-if="mode === 'mason'">
|
|
819
|
-
<div
|
|
820
|
-
v-if="i + (1 % gridHeight) === 0"
|
|
821
|
-
class="masonry-column relative"
|
|
822
|
-
>
|
|
823
|
-
<div v-if="ranking" class="img-gallery-ranking">
|
|
824
|
-
{{ i }}
|
|
825
|
-
</div>
|
|
826
|
-
|
|
827
|
-
<template v-for="j in gridHeight" :key="`gi_${id}_${i + j}`">
|
|
828
|
-
<div class="masonry-item">
|
|
829
|
-
<img
|
|
830
|
-
v-if="i + j - 2 < images.length && imageComponent === 'img'"
|
|
831
|
-
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]"
|
|
832
|
-
:src="getThumbnailUrl(images[i + j - 2])"
|
|
833
|
-
:alt="`Gallery image ${i + j - 1}`"
|
|
834
|
-
@click="$eventBus.emit(`${id}GalleryImage`, i + j - 2)"
|
|
835
|
-
>
|
|
836
|
-
<component
|
|
837
|
-
:is="imageComponent"
|
|
838
|
-
v-else-if="i + j - 2 < images.length"
|
|
839
|
-
:image="getThumbnailUrl(images[i + j - 2]).image"
|
|
840
|
-
:variant="getThumbnailUrl(images[i + j - 2]).variant"
|
|
841
|
-
:alt="getThumbnailUrl(images[i + j - 2]).alt"
|
|
842
|
-
: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(
|
|
843
|
-
images[i + j - 2],
|
|
844
|
-
)}`"
|
|
845
|
-
:likes="getThumbnailUrl(images[i + j - 2]).likes"
|
|
846
|
-
:show-likes="getThumbnailUrl(images[i + j - 2]).showLikes"
|
|
847
|
-
:is-author="getThumbnailUrl(images[i + j - 2]).isAuthor"
|
|
848
|
-
:user-uuid="getThumbnailUrl(images[i + j - 2]).userUUID"
|
|
849
|
-
@click="$eventBus.emit(`${id}GalleryImage`, i + j - 2)"
|
|
850
|
-
/>
|
|
851
|
-
</div>
|
|
852
|
-
</template>
|
|
853
|
-
</div>
|
|
854
|
-
</template>
|
|
855
|
-
<div v-else class="grid-item relative group">
|
|
856
|
-
<div v-if="ranking" class="img-gallery-ranking">
|
|
857
|
-
{{ i }}
|
|
858
|
-
</div>
|
|
859
|
-
<div class="overflow-hidden rounded-lg">
|
|
860
|
-
<img
|
|
861
|
-
v-if="imageComponent === 'img'"
|
|
862
|
-
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]"
|
|
863
|
-
:src="getThumbnailUrl(images[i - 1])"
|
|
864
|
-
:alt="`Gallery image ${i}`"
|
|
865
|
-
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
866
|
-
>
|
|
867
|
-
<component
|
|
868
|
-
:is="imageComponent"
|
|
869
|
-
v-else-if="imageComponent"
|
|
870
|
-
:image="getThumbnailUrl(images[i - 1]).image"
|
|
871
|
-
:variant="getThumbnailUrl(images[i - 1]).variant"
|
|
872
|
-
:alt="getThumbnailUrl(images[i - 1]).alt"
|
|
873
|
-
: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(
|
|
874
|
-
images[i - 1],
|
|
875
|
-
)}`"
|
|
876
|
-
:likes="getThumbnailUrl(images[i - 1]).likes"
|
|
877
|
-
:show-likes="getThumbnailUrl(images[i - 1]).showLikes"
|
|
878
|
-
:is-author="getThumbnailUrl(images[i - 1]).isAuthor"
|
|
879
|
-
:user-uuid="getThumbnailUrl(images[i - 1]).userUUID"
|
|
880
|
-
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
881
|
-
/>
|
|
882
|
-
</div>
|
|
883
|
-
</div>
|
|
884
|
-
</template>
|
|
885
|
-
</div>
|
|
886
|
-
</div>
|
|
887
|
-
|
|
888
|
-
<!-- Button Mode -->
|
|
889
|
-
<button
|
|
890
|
-
v-if="mode === 'button'"
|
|
891
|
-
:class="`btn ${buttonType ? buttonType : 'primary'} defaults relative overflow-hidden group`"
|
|
892
|
-
@click="openGalleryImage(0)"
|
|
893
|
-
>
|
|
894
|
-
<span class="relative z-10">{{ buttonText ? buttonText : $t("open_gallery_cta") }}</span>
|
|
895
|
-
<span class="absolute inset-0 bg-white/10 transform -translate-x-full group-hover:translate-x-0 transition-transform duration-300" />
|
|
896
|
-
</button>
|
|
897
|
-
</div>
|
|
898
|
-
</template>
|
|
899
|
-
|
|
900
|
-
<style scoped>
|
|
901
|
-
/* Fix for top controls to ensure they're always visible */
|
|
902
|
-
.fixed.top-0 {
|
|
903
|
-
position: fixed !important;
|
|
904
|
-
top: 0 !important;
|
|
905
|
-
left: 0 !important;
|
|
906
|
-
right: 0 !important;
|
|
907
|
-
z-index: 51 !important;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
/* Transition styles for next (right) navigation */
|
|
911
|
-
.slide-next-enter-active,
|
|
912
|
-
.slide-next-leave-active {
|
|
913
|
-
transition:
|
|
914
|
-
opacity 0.15s,
|
|
915
|
-
transform 0.15s,
|
|
916
|
-
filter 0.15s;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
.slide-next-enter-from {
|
|
920
|
-
opacity: 0;
|
|
921
|
-
transform: translateX(100%);
|
|
922
|
-
filter: blur(10px);
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
.slide-next-enter-to {
|
|
926
|
-
opacity: 1;
|
|
927
|
-
transform: translateX(0);
|
|
928
|
-
filter: blur(0);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
.slide-next-leave-from {
|
|
932
|
-
opacity: 1;
|
|
933
|
-
transform: translateX(0);
|
|
934
|
-
filter: blur(0);
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
.slide-next-leave-to {
|
|
938
|
-
opacity: 0;
|
|
939
|
-
transform: translateX(-100%);
|
|
940
|
-
filter: blur(10px);
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
/* Transition styles for prev (left) navigation */
|
|
944
|
-
.slide-prev-enter-active,
|
|
945
|
-
.slide-prev-leave-active {
|
|
946
|
-
transition:
|
|
947
|
-
opacity 0.15s,
|
|
948
|
-
transform 0.15s,
|
|
949
|
-
filter 0.15s;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
.slide-prev-enter-from {
|
|
953
|
-
opacity: 0;
|
|
954
|
-
transform: translateX(-100%);
|
|
955
|
-
filter: blur(10px);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
.slide-prev-enter-to {
|
|
959
|
-
opacity: 1;
|
|
960
|
-
transform: translateX(0);
|
|
961
|
-
filter: blur(0);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
.slide-prev-leave-from {
|
|
965
|
-
opacity: 1;
|
|
966
|
-
transform: translateX(0);
|
|
967
|
-
filter: blur(0);
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
.slide-prev-leave-to {
|
|
971
|
-
opacity: 0;
|
|
972
|
-
transform: translateX(100%);
|
|
973
|
-
filter: blur(10px);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
/* Modern grids */
|
|
977
|
-
.gallery-grid {
|
|
978
|
-
min-height: 200px;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
.standard-grid {
|
|
982
|
-
display: grid;
|
|
983
|
-
grid-template-columns: repeat(1, 1fr);
|
|
984
|
-
gap: 0.75rem;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
@media (min-width: 480px) {
|
|
988
|
-
.standard-grid {
|
|
989
|
-
grid-template-columns: repeat(2, 1fr);
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
@media (min-width: 768px) {
|
|
994
|
-
.standard-grid {
|
|
995
|
-
grid-template-columns: repeat(3, 1fr);
|
|
996
|
-
gap: 1rem;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
@media (min-width: 1024px) {
|
|
1001
|
-
.standard-grid {
|
|
1002
|
-
grid-template-columns: repeat(4, 1fr);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
@media (min-width: 1280px) {
|
|
1007
|
-
.standard-grid {
|
|
1008
|
-
grid-template-columns: repeat(5, 1fr);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
@media (min-width: 1536px) {
|
|
1013
|
-
.standard-grid {
|
|
1014
|
-
grid-template-columns: repeat(6, 1fr);
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
.masonry-grid {
|
|
1019
|
-
display: grid;
|
|
1020
|
-
grid-template-columns: repeat(1, 1fr);
|
|
1021
|
-
gap: 0.75rem;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
@media (min-width: 480px) {
|
|
1025
|
-
.masonry-grid {
|
|
1026
|
-
grid-template-columns: repeat(2, 1fr);
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
@media (min-width: 768px) {
|
|
1031
|
-
.masonry-grid {
|
|
1032
|
-
grid-template-columns: repeat(3, 1fr);
|
|
1033
|
-
gap: 1rem;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
@media (min-width: 1024px) {
|
|
1038
|
-
.masonry-grid {
|
|
1039
|
-
grid-template-columns: repeat(4, 1fr);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
.masonry-column {
|
|
1044
|
-
display: grid;
|
|
1045
|
-
gap: 0.75rem;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
.masonry-item {
|
|
1049
|
-
break-inside: avoid;
|
|
1050
|
-
margin-bottom: 0.75rem;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
.grid-item {
|
|
1054
|
-
break-inside: avoid;
|
|
1055
|
-
margin-bottom: 0.75rem;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
.img-gallery-ranking {
|
|
1059
|
-
position: absolute;
|
|
1060
|
-
top: 0.5rem;
|
|
1061
|
-
left: 0.5rem;
|
|
1062
|
-
background-color: rgba(0, 0, 0, 0.6);
|
|
1063
|
-
color: white;
|
|
1064
|
-
width: 1.5rem;
|
|
1065
|
-
height: 1.5rem;
|
|
1066
|
-
display: flex;
|
|
1067
|
-
align-items: center;
|
|
1068
|
-
justify-content: center;
|
|
1069
|
-
border-radius: 9999px;
|
|
1070
|
-
font-size: 0.75rem;
|
|
1071
|
-
z-index: 10;
|
|
1072
|
-
}
|
|
1073
|
-
</style>
|