@fy-/fws-vue 2.3.13 → 2.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/ui/DefaultGallery.vue +447 -393
- package/components/ui/DefaultGalleryOld.vue +1073 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1073 @@
|
|
|
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>
|