@fy-/fws-vue 2.3.12 → 2.3.14
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 +440 -370
- package/components/ui/DefaultGalleryOld.vue +1073 -0
- package/package.json +1 -1
|
@@ -9,35 +9,78 @@ import {
|
|
|
9
9
|
InformationCircleIcon,
|
|
10
10
|
XMarkIcon,
|
|
11
11
|
} from '@heroicons/vue/24/solid'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
useDebounceFn,
|
|
14
|
+
useElementSize,
|
|
15
|
+
useEventListener,
|
|
16
|
+
useFullscreen,
|
|
17
|
+
useResizeObserver,
|
|
18
|
+
useWindowSize,
|
|
19
|
+
} from '@vueuse/core'
|
|
13
20
|
import { computed, h, nextTick, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from 'vue'
|
|
14
21
|
import { useEventBus } from '../../composables/event-bus'
|
|
22
|
+
import { ClientOnly } from '../ssr/ClientOnly'
|
|
15
23
|
import DefaultPaging from './DefaultPaging.vue'
|
|
16
24
|
|
|
25
|
+
// Core state
|
|
17
26
|
const isGalleryOpen = ref<boolean>(false)
|
|
18
27
|
const eventBus = useEventBus()
|
|
19
28
|
const sidePanel = ref<boolean>(true)
|
|
20
29
|
const showControls = ref<boolean>(true)
|
|
21
30
|
const isFullscreen = ref<boolean>(false)
|
|
22
31
|
const infoPanel = ref<boolean>(true) // Show info panel by default
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
const direction = ref<'next' | 'prev'>('next')
|
|
33
|
+
|
|
34
|
+
// Refs to track DOM elements and their sizes
|
|
35
|
+
const galleryRef = shallowRef<HTMLElement | null>(null)
|
|
36
|
+
const galleryContentRef = shallowRef<HTMLElement | null>(null)
|
|
37
|
+
const imageContainerRef = shallowRef<HTMLElement | null>(null)
|
|
38
|
+
const infoPanelRef = shallowRef<HTMLElement | null>(null)
|
|
39
|
+
const sidePanelRef = shallowRef<HTMLElement | null>(null)
|
|
40
|
+
const topControlsRef = shallowRef<HTMLElement | null>(null)
|
|
41
|
+
|
|
42
|
+
// Use VueUse's useElementSize for reliable sizing
|
|
43
|
+
const { width: galleryWidth, height: galleryHeight } = useElementSize(galleryRef)
|
|
44
|
+
const { width: windowWidth, height: windowHeight } = useWindowSize()
|
|
45
|
+
const { height: topControlsHeight } = useElementSize(topControlsRef)
|
|
46
|
+
const { height: infoPanelHeight } = useElementSize(infoPanelRef)
|
|
47
|
+
|
|
48
|
+
// Derived measurements
|
|
49
|
+
const availableHeight = computed(() => {
|
|
50
|
+
let height = isFullscreen.value
|
|
51
|
+
? windowHeight.value * 0.95 // 95% of viewport in fullscreen
|
|
52
|
+
: windowHeight.value * 0.85 // 85% of viewport in normal mode
|
|
53
|
+
|
|
54
|
+
// Subtract top controls
|
|
55
|
+
height -= topControlsHeight.value
|
|
56
|
+
|
|
57
|
+
// Subtract info panel if visible
|
|
58
|
+
if (infoPanel.value && infoPanelHeight.value > 0) {
|
|
59
|
+
height -= infoPanelHeight.value
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Convert to rem for consistent sizing
|
|
63
|
+
return `${height / 16}rem`
|
|
64
|
+
})
|
|
26
65
|
|
|
27
66
|
// Use VueUse's useFullscreen for better fullscreen handling
|
|
28
|
-
const { isFullscreen: isElementFullscreen, enter: enterFullscreen, exit: exitFullscreen } = useFullscreen(
|
|
67
|
+
const { isFullscreen: isElementFullscreen, enter: enterFullscreen, exit: exitFullscreen } = useFullscreen(galleryRef)
|
|
29
68
|
|
|
30
69
|
// Track when fullscreen changes externally (like Escape key)
|
|
31
70
|
watch(isElementFullscreen, (newValue) => {
|
|
32
71
|
isFullscreen.value = newValue
|
|
33
|
-
if (newValue) {
|
|
34
|
-
// Force update of image size when entering fullscreen
|
|
35
|
-
nextTick(() => {
|
|
36
|
-
updateImageSizes()
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
72
|
})
|
|
40
73
|
|
|
74
|
+
// Touch handling state
|
|
75
|
+
const touchStartTime = ref<number>(0)
|
|
76
|
+
const start = reactive({ x: 0, y: 0 })
|
|
77
|
+
const isKeyPressed = ref<boolean>(false)
|
|
78
|
+
|
|
79
|
+
// Timers for automatic control hiding
|
|
80
|
+
let controlsTimeout: number | null = null
|
|
81
|
+
let fullscreenResizeTimeout: number | null = null
|
|
82
|
+
|
|
83
|
+
// Props definition with defaults
|
|
41
84
|
const props = withDefaults(
|
|
42
85
|
defineProps<{
|
|
43
86
|
id: string
|
|
@@ -77,7 +120,10 @@ const props = withDefaults(
|
|
|
77
120
|
},
|
|
78
121
|
)
|
|
79
122
|
|
|
123
|
+
// Emits
|
|
80
124
|
const emit = defineEmits(['update:modelValue'])
|
|
125
|
+
|
|
126
|
+
// Two-way binding for model value
|
|
81
127
|
const modelValue = computed({
|
|
82
128
|
get: () => props.modelValue,
|
|
83
129
|
set: (i) => {
|
|
@@ -85,40 +131,61 @@ const modelValue = computed({
|
|
|
85
131
|
},
|
|
86
132
|
})
|
|
87
133
|
|
|
88
|
-
|
|
134
|
+
// Computed values
|
|
135
|
+
const modelValueSrc = computed(() => {
|
|
136
|
+
if (props.images.length === 0) return false
|
|
137
|
+
if (props.images[modelValue.value] === undefined) return false
|
|
138
|
+
return props.getImageUrl(props.images[modelValue.value])
|
|
139
|
+
})
|
|
89
140
|
|
|
90
|
-
|
|
91
|
-
|
|
141
|
+
const currentImage = computed(() => {
|
|
142
|
+
if (props.images.length === 0) return null
|
|
143
|
+
return props.images[modelValue.value]
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const imageCount = computed(() => props.images.length)
|
|
147
|
+
const currentIndex = computed(() => modelValue.value + 1)
|
|
148
|
+
|
|
149
|
+
// Image size and positioning
|
|
150
|
+
const calculateImageSize = useDebounceFn(() => {
|
|
151
|
+
if (!imageContainerRef.value) return
|
|
92
152
|
|
|
93
|
-
// Used to maintain consistent image sizes
|
|
94
|
-
function updateImageSizes() {
|
|
95
153
|
nextTick(() => {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
154
|
+
const imageElements = imageContainerRef.value?.querySelectorAll('.image-display img, .image-display .video-component') as NodeListOf<HTMLElement>
|
|
155
|
+
|
|
156
|
+
if (!imageElements || imageElements.length === 0) return
|
|
157
|
+
|
|
158
|
+
imageElements.forEach((img) => {
|
|
159
|
+
// Reset to ensure proper recalculation
|
|
160
|
+
img.style.maxHeight = ''
|
|
161
|
+
// Force browser to recalculate styles
|
|
162
|
+
void img.offsetHeight
|
|
163
|
+
|
|
164
|
+
// Set proper height
|
|
165
|
+
img.style.maxHeight = availableHeight.value
|
|
166
|
+
img.style.maxWidth = sidePanel.value ? 'calc(100vw - 16rem)' : '100vw'
|
|
167
|
+
})
|
|
109
168
|
})
|
|
110
|
-
}
|
|
169
|
+
}, 50)
|
|
111
170
|
|
|
171
|
+
// Update all layout measurements
|
|
172
|
+
const updateLayout = useDebounceFn(() => {
|
|
173
|
+
calculateImageSize()
|
|
174
|
+
}, 50)
|
|
175
|
+
|
|
176
|
+
// Modal controls
|
|
112
177
|
function setModal(value: boolean) {
|
|
113
178
|
if (value === true) {
|
|
114
179
|
if (props.onOpen) props.onOpen()
|
|
115
180
|
document.body.style.overflow = 'hidden' // Prevent scrolling when gallery is open
|
|
181
|
+
|
|
116
182
|
if (!import.meta.env.SSR) {
|
|
117
183
|
useEventListener(document, 'keydown', handleKeyboardInput)
|
|
118
184
|
useEventListener(document, 'keyup', handleKeyboardRelease)
|
|
119
185
|
}
|
|
186
|
+
|
|
120
187
|
// Auto-hide controls after 3 seconds on mobile
|
|
121
|
-
if (
|
|
188
|
+
if (windowWidth.value < 1024) {
|
|
122
189
|
controlsTimeout = window.setTimeout(() => {
|
|
123
190
|
showControls.value = false
|
|
124
191
|
}, 3000)
|
|
@@ -127,11 +194,13 @@ function setModal(value: boolean) {
|
|
|
127
194
|
else {
|
|
128
195
|
if (props.onClose) props.onClose()
|
|
129
196
|
document.body.style.overflow = '' // Restore scrolling
|
|
197
|
+
|
|
130
198
|
// Exit fullscreen if active
|
|
131
199
|
if (isFullscreen.value) {
|
|
132
200
|
exitFullscreen()
|
|
133
201
|
isFullscreen.value = false
|
|
134
202
|
}
|
|
203
|
+
|
|
135
204
|
// Clear timeout if modal is closed
|
|
136
205
|
if (controlsTimeout) {
|
|
137
206
|
clearTimeout(controlsTimeout)
|
|
@@ -143,6 +212,7 @@ function setModal(value: boolean) {
|
|
|
143
212
|
// Don't reset info panel state when opening/closing
|
|
144
213
|
}
|
|
145
214
|
|
|
215
|
+
// Open gallery with debounce to prevent accidental double-clicks
|
|
146
216
|
const openGalleryImage = useDebounceFn((index: number | undefined) => {
|
|
147
217
|
if (index === undefined) {
|
|
148
218
|
modelValue.value = 0
|
|
@@ -151,8 +221,14 @@ const openGalleryImage = useDebounceFn((index: number | undefined) => {
|
|
|
151
221
|
modelValue.value = Number.parseInt(index.toString())
|
|
152
222
|
}
|
|
153
223
|
setModal(true)
|
|
154
|
-
}, 50) // Debounce to prevent accidental double-opens
|
|
155
224
|
|
|
225
|
+
// Update layout after opening
|
|
226
|
+
nextTick(() => {
|
|
227
|
+
updateLayout()
|
|
228
|
+
})
|
|
229
|
+
}, 50)
|
|
230
|
+
|
|
231
|
+
// Navigation functions
|
|
156
232
|
function goNextImage() {
|
|
157
233
|
direction.value = 'next'
|
|
158
234
|
if (modelValue.value < props.images.length - 1) {
|
|
@@ -170,34 +246,18 @@ function goPrevImage() {
|
|
|
170
246
|
modelValue.value--
|
|
171
247
|
}
|
|
172
248
|
else {
|
|
173
|
-
modelValue.value
|
|
174
|
-
= props.images.length - 1 > 0 ? props.images.length - 1 : 0
|
|
249
|
+
modelValue.value = props.images.length - 1 > 0 ? props.images.length - 1 : 0
|
|
175
250
|
}
|
|
176
251
|
resetControlsTimer()
|
|
177
252
|
}
|
|
178
253
|
|
|
179
|
-
|
|
180
|
-
if (props.images.length === 0) return false
|
|
181
|
-
if (props.images[modelValue.value] === undefined) return false
|
|
182
|
-
return props.getImageUrl(props.images[modelValue.value])
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
const currentImage = computed(() => {
|
|
186
|
-
if (props.images.length === 0) return null
|
|
187
|
-
return props.images[modelValue.value]
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
const imageCount = computed(() => props.images.length)
|
|
191
|
-
const currentIndex = computed(() => modelValue.value + 1)
|
|
192
|
-
|
|
193
|
-
const start = reactive({ x: 0, y: 0 })
|
|
194
|
-
|
|
254
|
+
// UI control functions
|
|
195
255
|
function resetControlsTimer() {
|
|
196
256
|
// Show controls when user interacts
|
|
197
257
|
showControls.value = true
|
|
198
258
|
|
|
199
259
|
// Only set timer on mobile
|
|
200
|
-
if (
|
|
260
|
+
if (windowWidth.value < 1024) {
|
|
201
261
|
if (controlsTimeout) {
|
|
202
262
|
clearTimeout(controlsTimeout)
|
|
203
263
|
}
|
|
@@ -209,7 +269,7 @@ function resetControlsTimer() {
|
|
|
209
269
|
|
|
210
270
|
function toggleControls() {
|
|
211
271
|
showControls.value = !showControls.value
|
|
212
|
-
if (showControls.value &&
|
|
272
|
+
if (showControls.value && windowWidth.value < 1024) {
|
|
213
273
|
resetControlsTimer()
|
|
214
274
|
}
|
|
215
275
|
}
|
|
@@ -218,28 +278,32 @@ function toggleInfoPanel() {
|
|
|
218
278
|
infoPanel.value = !infoPanel.value
|
|
219
279
|
resetControlsTimer()
|
|
220
280
|
|
|
221
|
-
// Update
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
281
|
+
// Update layout after panel toggle
|
|
282
|
+
nextTick(() => {
|
|
283
|
+
updateLayout()
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function toggleSidePanel() {
|
|
288
|
+
sidePanel.value = !sidePanel.value
|
|
289
|
+
resetControlsTimer()
|
|
290
|
+
|
|
291
|
+
// Update layout after panel toggle
|
|
292
|
+
nextTick(() => {
|
|
293
|
+
updateLayout()
|
|
294
|
+
})
|
|
231
295
|
}
|
|
232
296
|
|
|
233
297
|
function toggleFullscreen() {
|
|
234
298
|
if (!isFullscreen.value) {
|
|
235
|
-
if (
|
|
299
|
+
if (galleryRef.value) {
|
|
236
300
|
enterFullscreen()
|
|
237
301
|
.then(() => {
|
|
238
302
|
isFullscreen.value = true
|
|
239
303
|
// Give browser time to adjust fullscreen before updating sizing
|
|
240
304
|
if (fullscreenResizeTimeout) clearTimeout(fullscreenResizeTimeout)
|
|
241
305
|
fullscreenResizeTimeout = window.setTimeout(() => {
|
|
242
|
-
|
|
306
|
+
updateLayout()
|
|
243
307
|
}, 50)
|
|
244
308
|
})
|
|
245
309
|
.catch(() => {})
|
|
@@ -249,6 +313,10 @@ function toggleFullscreen() {
|
|
|
249
313
|
exitFullscreen()
|
|
250
314
|
.then(() => {
|
|
251
315
|
isFullscreen.value = false
|
|
316
|
+
if (fullscreenResizeTimeout) clearTimeout(fullscreenResizeTimeout)
|
|
317
|
+
fullscreenResizeTimeout = window.setTimeout(() => {
|
|
318
|
+
updateLayout()
|
|
319
|
+
}, 50)
|
|
252
320
|
})
|
|
253
321
|
.catch(() => {})
|
|
254
322
|
}
|
|
@@ -265,7 +333,7 @@ const touchStart = useDebounceFn((event: TouchEvent) => {
|
|
|
265
333
|
|
|
266
334
|
// Check if the touch started on an interactive element
|
|
267
335
|
if (targetElement.closest('button, a, input, textarea, select')) {
|
|
268
|
-
return // Don't handle swipe if interacting with
|
|
336
|
+
return // Don't handle swipe if interacting with controls
|
|
269
337
|
}
|
|
270
338
|
|
|
271
339
|
start.x = touch.screenX
|
|
@@ -279,7 +347,7 @@ const touchEnd = useDebounceFn((event: TouchEvent) => {
|
|
|
279
347
|
|
|
280
348
|
// Check if the touch ended on an interactive element
|
|
281
349
|
if (targetElement.closest('button, a, input, textarea, select')) {
|
|
282
|
-
return // Don't handle swipe if interacting with
|
|
350
|
+
return // Don't handle swipe if interacting with controls
|
|
283
351
|
}
|
|
284
352
|
|
|
285
353
|
const end = { x: touch.screenX, y: touch.screenY }
|
|
@@ -296,16 +364,15 @@ const touchEnd = useDebounceFn((event: TouchEvent) => {
|
|
|
296
364
|
// Add a threshold to prevent accidental swipes
|
|
297
365
|
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
|
|
298
366
|
if (diffX > 0) {
|
|
299
|
-
direction.value = 'next'
|
|
300
367
|
goNextImage()
|
|
301
368
|
}
|
|
302
369
|
else {
|
|
303
|
-
direction.value = 'prev'
|
|
304
370
|
goPrevImage()
|
|
305
371
|
}
|
|
306
372
|
}
|
|
307
373
|
}, 50)
|
|
308
374
|
|
|
375
|
+
// Border color function
|
|
309
376
|
function getBorderColor(i: any) {
|
|
310
377
|
if (props.borderColor !== undefined) {
|
|
311
378
|
return props.borderColor(i)
|
|
@@ -313,8 +380,7 @@ function getBorderColor(i: any) {
|
|
|
313
380
|
return ''
|
|
314
381
|
}
|
|
315
382
|
|
|
316
|
-
|
|
317
|
-
|
|
383
|
+
// Keyboard handlers
|
|
318
384
|
function handleKeyboardInput(event: KeyboardEvent) {
|
|
319
385
|
if (!isGalleryOpen.value) return
|
|
320
386
|
if (isKeyPressed.value) return
|
|
@@ -326,12 +392,10 @@ function handleKeyboardInput(event: KeyboardEvent) {
|
|
|
326
392
|
break
|
|
327
393
|
case 'ArrowRight':
|
|
328
394
|
isKeyPressed.value = true
|
|
329
|
-
direction.value = 'next'
|
|
330
395
|
goNextImage()
|
|
331
396
|
break
|
|
332
397
|
case 'ArrowLeft':
|
|
333
398
|
isKeyPressed.value = true
|
|
334
|
-
direction.value = 'prev'
|
|
335
399
|
goPrevImage()
|
|
336
400
|
break
|
|
337
401
|
case 'f':
|
|
@@ -362,65 +426,55 @@ const handleBackdropClick = useDebounceFn((event: MouseEvent) => {
|
|
|
362
426
|
}
|
|
363
427
|
}, 200)
|
|
364
428
|
|
|
365
|
-
// Watch for
|
|
366
|
-
watch(
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (infoElement) {
|
|
384
|
-
const height = infoElement.offsetHeight
|
|
385
|
-
infoHeight.value = height
|
|
386
|
-
document.documentElement.style.setProperty('--info-height', `${height}px`)
|
|
387
|
-
}
|
|
388
|
-
})
|
|
389
|
-
}
|
|
429
|
+
// Watch for image changes, fullscreen, or panel visibility changes
|
|
430
|
+
watch(
|
|
431
|
+
[
|
|
432
|
+
currentImage,
|
|
433
|
+
isFullscreen,
|
|
434
|
+
infoPanel,
|
|
435
|
+
sidePanel,
|
|
436
|
+
windowWidth,
|
|
437
|
+
windowHeight,
|
|
438
|
+
galleryWidth,
|
|
439
|
+
galleryHeight,
|
|
440
|
+
topControlsHeight,
|
|
441
|
+
infoPanelHeight,
|
|
442
|
+
],
|
|
443
|
+
() => {
|
|
444
|
+
updateLayout()
|
|
445
|
+
},
|
|
446
|
+
)
|
|
390
447
|
|
|
448
|
+
// Lifecycle hooks
|
|
391
449
|
onMounted(() => {
|
|
392
450
|
eventBus.on(`${props.id}GalleryImage`, openGalleryImage)
|
|
393
451
|
eventBus.on(`${props.id}Gallery`, openGalleryImage)
|
|
394
452
|
eventBus.on(`${props.id}GalleryClose`, closeGallery)
|
|
395
453
|
|
|
396
|
-
//
|
|
397
|
-
|
|
454
|
+
// Initialize layout
|
|
455
|
+
nextTick(() => {
|
|
456
|
+
updateLayout()
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
// Set up observers for dynamic resizing
|
|
460
|
+
if (topControlsRef.value) {
|
|
461
|
+
useResizeObserver(topControlsRef.value, updateLayout)
|
|
462
|
+
}
|
|
398
463
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
updateInfoHeight()
|
|
464
|
+
if (infoPanelRef.value) {
|
|
465
|
+
useResizeObserver(infoPanelRef.value, updateLayout)
|
|
402
466
|
}
|
|
403
467
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (infoElement) {
|
|
407
|
-
useResizeObserver(infoElement as HTMLElement, () => {
|
|
408
|
-
if (infoPanel.value) {
|
|
409
|
-
updateInfoHeight()
|
|
410
|
-
}
|
|
411
|
-
})
|
|
468
|
+
if (sidePanelRef.value) {
|
|
469
|
+
useResizeObserver(sidePanelRef.value, updateLayout)
|
|
412
470
|
}
|
|
413
471
|
|
|
414
|
-
// Listen for fullscreen changes
|
|
472
|
+
// Listen for fullscreen changes
|
|
415
473
|
useEventListener(document, 'fullscreenchange', () => {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
isFullscreen.value = false
|
|
423
|
-
}
|
|
474
|
+
isFullscreen.value = !!document.fullscreenElement
|
|
475
|
+
nextTick(() => {
|
|
476
|
+
updateLayout()
|
|
477
|
+
})
|
|
424
478
|
})
|
|
425
479
|
})
|
|
426
480
|
|
|
@@ -461,284 +515,262 @@ onUnmounted(() => {
|
|
|
461
515
|
>
|
|
462
516
|
<div
|
|
463
517
|
v-if="isGalleryOpen"
|
|
464
|
-
|
|
518
|
+
ref="galleryRef"
|
|
519
|
+
class="fixed bg-fv-neutral-900 text-white inset-0 max-w-[100vw] max-h-[100vh] overflow-hidden gallery-container"
|
|
465
520
|
style="z-index: 37"
|
|
466
521
|
role="dialog"
|
|
467
522
|
aria-modal="true"
|
|
468
523
|
@click="handleBackdropClick"
|
|
469
524
|
>
|
|
525
|
+
<!-- Top Controls Bar - Fixed at top -->
|
|
526
|
+
<transition
|
|
527
|
+
enter-active-class="transition-opacity duration-300"
|
|
528
|
+
enter-from-class="opacity-0"
|
|
529
|
+
enter-to-class="opacity-100"
|
|
530
|
+
leave-active-class="transition-opacity duration-300"
|
|
531
|
+
leave-from-class="opacity-100"
|
|
532
|
+
leave-to-class="opacity-0"
|
|
533
|
+
>
|
|
534
|
+
<div
|
|
535
|
+
v-if="showControls"
|
|
536
|
+
ref="topControlsRef"
|
|
537
|
+
class="fixed top-0 left-0 right-0 px-4 py-2 flex justify-between items-center bg-fv-neutral-900/90 backdrop-blur-sm z-50 controls-bar"
|
|
538
|
+
>
|
|
539
|
+
<!-- Title and Counter -->
|
|
540
|
+
<div class="flex items-center space-x-2">
|
|
541
|
+
<span v-if="title" class="font-medium text-lg">{{ title }}</span>
|
|
542
|
+
<span class="text-sm opacity-80">{{ currentIndex }} / {{ imageCount }}</span>
|
|
543
|
+
</div>
|
|
544
|
+
|
|
545
|
+
<!-- Control Buttons -->
|
|
546
|
+
<div class="flex items-center space-x-2">
|
|
547
|
+
<button
|
|
548
|
+
class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
549
|
+
:class="{ 'bg-fv-primary-500/70': infoPanel }"
|
|
550
|
+
:title="infoPanel ? 'Hide info' : 'Show info'"
|
|
551
|
+
@click="toggleInfoPanel"
|
|
552
|
+
>
|
|
553
|
+
<InformationCircleIcon class="w-5 h-5" />
|
|
554
|
+
</button>
|
|
555
|
+
|
|
556
|
+
<button
|
|
557
|
+
class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
558
|
+
:title="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
|
|
559
|
+
@click="toggleSidePanel"
|
|
560
|
+
>
|
|
561
|
+
<ChevronDoubleRightIcon v-if="sidePanel" class="w-5 h-5" />
|
|
562
|
+
<ChevronDoubleLeftIcon v-else class="w-5 h-5" />
|
|
563
|
+
</button>
|
|
564
|
+
|
|
565
|
+
<button
|
|
566
|
+
class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
567
|
+
aria-label="Close gallery"
|
|
568
|
+
@click="setModal(false)"
|
|
569
|
+
>
|
|
570
|
+
<component :is="closeIcon" class="w-5 h-5" />
|
|
571
|
+
</button>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
</transition>
|
|
575
|
+
|
|
576
|
+
<!-- Main Gallery Content - Flexbox layout -->
|
|
470
577
|
<div
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
578
|
+
ref="galleryContentRef"
|
|
579
|
+
class="w-full h-full flex flex-col lg:flex-row"
|
|
580
|
+
style="margin-top: var(--controls-height, 0px)"
|
|
474
581
|
>
|
|
475
|
-
<!-- Main
|
|
476
|
-
<div
|
|
477
|
-
|
|
478
|
-
|
|
582
|
+
<!-- Main Image Area - Fills available space -->
|
|
583
|
+
<div
|
|
584
|
+
class="relative flex-1 h-full flex items-center justify-center"
|
|
585
|
+
:class="{ 'lg:pr-64': sidePanel }"
|
|
586
|
+
>
|
|
587
|
+
<!-- Image Navigation Controls - Left -->
|
|
588
|
+
<transition
|
|
589
|
+
enter-active-class="transition-opacity duration-300"
|
|
590
|
+
enter-from-class="opacity-0"
|
|
591
|
+
enter-to-class="opacity-100"
|
|
592
|
+
leave-active-class="transition-opacity duration-300"
|
|
593
|
+
leave-from-class="opacity-100"
|
|
594
|
+
leave-to-class="opacity-0"
|
|
595
|
+
>
|
|
479
596
|
<div
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
@touchend="touchEnd"
|
|
597
|
+
v-if="showControls && images.length > 1"
|
|
598
|
+
class="absolute left-0 z-40 h-full flex items-center px-2 md:px-4"
|
|
483
599
|
>
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
enter-to-class="opacity-100"
|
|
489
|
-
leave-active-class="transition-opacity duration-300"
|
|
490
|
-
leave-from-class="opacity-100"
|
|
491
|
-
leave-to-class="opacity-0"
|
|
600
|
+
<button
|
|
601
|
+
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"
|
|
602
|
+
aria-label="Previous image"
|
|
603
|
+
@click="goPrevImage()"
|
|
492
604
|
>
|
|
493
|
-
<
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
<button
|
|
498
|
-
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"
|
|
499
|
-
aria-label="Previous image"
|
|
500
|
-
@click="goPrevImage()"
|
|
501
|
-
>
|
|
502
|
-
<ChevronLeftIcon class="w-6 h-6 md:w-8 md:h-8" />
|
|
503
|
-
</button>
|
|
504
|
-
</div>
|
|
505
|
-
</transition>
|
|
605
|
+
<ChevronLeftIcon class="w-6 h-6 md:w-8 md:h-8" />
|
|
606
|
+
</button>
|
|
607
|
+
</div>
|
|
608
|
+
</transition>
|
|
506
609
|
|
|
507
|
-
|
|
610
|
+
<!-- Image Display Container -->
|
|
611
|
+
<div
|
|
612
|
+
ref="imageContainerRef"
|
|
613
|
+
class="image-container flex-grow flex items-center justify-center"
|
|
614
|
+
:class="{ 'has-info': infoPanel }"
|
|
615
|
+
@touchstart="touchStart"
|
|
616
|
+
@touchend="touchEnd"
|
|
617
|
+
>
|
|
618
|
+
<transition
|
|
619
|
+
:name="direction === 'next' ? 'slide-next' : 'slide-prev'"
|
|
620
|
+
mode="out-in"
|
|
621
|
+
>
|
|
508
622
|
<div
|
|
509
|
-
|
|
623
|
+
:key="`image-display-${modelValue}`"
|
|
624
|
+
class="image-display relative w-full h-full flex flex-col items-center justify-center"
|
|
510
625
|
>
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
626
|
+
<!-- Actual Image/Video Content -->
|
|
627
|
+
<template v-if="videoComponent && isVideo(images[modelValue])">
|
|
628
|
+
<ClientOnly>
|
|
629
|
+
<component
|
|
630
|
+
:is="videoComponent"
|
|
631
|
+
:src="isVideo(images[modelValue])"
|
|
632
|
+
class="shadow max-w-full h-auto object-contain video-component"
|
|
633
|
+
:style="{ maxHeight: availableHeight }"
|
|
634
|
+
/>
|
|
635
|
+
</ClientOnly>
|
|
636
|
+
</template>
|
|
637
|
+
<template v-else>
|
|
638
|
+
<img
|
|
639
|
+
v-if="modelValueSrc && imageComponent === 'img'"
|
|
640
|
+
class="shadow max-w-full h-auto object-contain"
|
|
641
|
+
:style="{ maxHeight: availableHeight }"
|
|
642
|
+
:src="modelValueSrc"
|
|
643
|
+
:alt="`Gallery image ${modelValue + 1}`"
|
|
518
644
|
>
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
? 'calc(90vh - var(--info-height, 0px) - 4rem)'
|
|
534
|
-
: 'calc(90vh - 4rem)'
|
|
535
|
-
: infoPanel
|
|
536
|
-
? 'calc(80vh - var(--info-height, 0px) - 4rem)'
|
|
537
|
-
: 'calc(80vh - 4rem)',
|
|
538
|
-
}"
|
|
539
|
-
/>
|
|
540
|
-
</ClientOnly>
|
|
541
|
-
</template>
|
|
542
|
-
<template v-else>
|
|
543
|
-
<img
|
|
544
|
-
v-if="modelValueSrc && imageComponent === 'img'"
|
|
545
|
-
class="shadow max-w-full h-auto object-contain"
|
|
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
|
-
:src="modelValueSrc"
|
|
556
|
-
:alt="`Gallery image ${modelValue + 1}`"
|
|
557
|
-
>
|
|
558
|
-
<component
|
|
559
|
-
:is="imageComponent"
|
|
560
|
-
v-else-if="modelValueSrc && imageComponent"
|
|
561
|
-
:image="modelValueSrc.image"
|
|
562
|
-
:variant="modelValueSrc.variant"
|
|
563
|
-
:alt="modelValueSrc.alt"
|
|
564
|
-
class="shadow max-w-full h-auto object-contain"
|
|
565
|
-
:style="{
|
|
566
|
-
maxHeight: isFullscreen
|
|
567
|
-
? infoPanel
|
|
568
|
-
? 'calc(90vh - var(--info-height, 0px) - 4rem)'
|
|
569
|
-
: 'calc(90vh - 4rem)'
|
|
570
|
-
: infoPanel
|
|
571
|
-
? 'calc(80vh - var(--info-height, 0px) - 4rem)'
|
|
572
|
-
: 'calc(80vh - 4rem)',
|
|
573
|
-
}"
|
|
574
|
-
/>
|
|
575
|
-
</template>
|
|
576
|
-
</div>
|
|
577
|
-
|
|
578
|
-
<!-- Image Slot Content -->
|
|
579
|
-
<div
|
|
580
|
-
v-if="infoPanel"
|
|
581
|
-
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"
|
|
582
|
-
@transitionend="updateInfoHeight"
|
|
583
|
-
>
|
|
584
|
-
<slot :value="images[modelValue]" />
|
|
585
|
-
</div>
|
|
586
|
-
</div>
|
|
587
|
-
</transition>
|
|
645
|
+
<component
|
|
646
|
+
:is="imageComponent"
|
|
647
|
+
v-else-if="modelValueSrc && imageComponent"
|
|
648
|
+
:image="modelValueSrc.image"
|
|
649
|
+
:variant="modelValueSrc.variant"
|
|
650
|
+
:alt="modelValueSrc.alt"
|
|
651
|
+
class="shadow max-w-full h-auto object-contain"
|
|
652
|
+
:style="{ maxHeight: availableHeight }"
|
|
653
|
+
:likes="modelValueSrc.likes"
|
|
654
|
+
:show-likes="modelValueSrc.showLikes"
|
|
655
|
+
:is-author="modelValueSrc.isAuthor"
|
|
656
|
+
:user-uuid="modelValueSrc.userUUID"
|
|
657
|
+
/>
|
|
658
|
+
</template>
|
|
588
659
|
</div>
|
|
660
|
+
</transition>
|
|
661
|
+
</div>
|
|
589
662
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
663
|
+
<!-- Image Navigation Controls - Right -->
|
|
664
|
+
<transition
|
|
665
|
+
enter-active-class="transition-opacity duration-300"
|
|
666
|
+
enter-from-class="opacity-0"
|
|
667
|
+
enter-to-class="opacity-100"
|
|
668
|
+
leave-active-class="transition-opacity duration-300"
|
|
669
|
+
leave-from-class="opacity-100"
|
|
670
|
+
leave-to-class="opacity-0"
|
|
671
|
+
>
|
|
672
|
+
<div
|
|
673
|
+
v-if="showControls && images.length > 1"
|
|
674
|
+
class="absolute right-0 z-40 h-full flex items-center px-2 md:px-4"
|
|
675
|
+
>
|
|
676
|
+
<button
|
|
677
|
+
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"
|
|
678
|
+
aria-label="Next image"
|
|
679
|
+
@click="goNextImage()"
|
|
598
680
|
>
|
|
599
|
-
<
|
|
600
|
-
|
|
601
|
-
class="absolute right-0 z-[40] h-full flex items-center px-2 md:px-4"
|
|
602
|
-
>
|
|
603
|
-
<button
|
|
604
|
-
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"
|
|
605
|
-
aria-label="Next image"
|
|
606
|
-
@click="goNextImage()"
|
|
607
|
-
>
|
|
608
|
-
<ChevronRightIcon class="w-6 h-6 md:w-8 md:h-8" />
|
|
609
|
-
</button>
|
|
610
|
-
</div>
|
|
611
|
-
</transition>
|
|
681
|
+
<ChevronRightIcon class="w-6 h-6 md:w-8 md:h-8" />
|
|
682
|
+
</button>
|
|
612
683
|
</div>
|
|
613
|
-
</
|
|
684
|
+
</transition>
|
|
614
685
|
|
|
615
|
-
<!--
|
|
686
|
+
<!-- Info Panel Below Image -->
|
|
616
687
|
<transition
|
|
617
|
-
enter-active-class="
|
|
618
|
-
enter-from-class="translate-
|
|
619
|
-
enter-to-class="translate-
|
|
620
|
-
leave-active-class="
|
|
621
|
-
leave-from-class="translate-
|
|
622
|
-
leave-to-class="translate-
|
|
688
|
+
enter-active-class="transition-all duration-300 ease-out"
|
|
689
|
+
enter-from-class="opacity-0 transform translate-y-4"
|
|
690
|
+
enter-to-class="opacity-100 transform translate-y-0"
|
|
691
|
+
leave-active-class="transition-all duration-300 ease-in"
|
|
692
|
+
leave-from-class="opacity-100 transform translate-y-0"
|
|
693
|
+
leave-to-class="opacity-0 transform translate-y-4"
|
|
623
694
|
>
|
|
624
695
|
<div
|
|
625
|
-
v-if="
|
|
626
|
-
|
|
696
|
+
v-if="infoPanel && images[modelValue]"
|
|
697
|
+
ref="infoPanelRef"
|
|
698
|
+
class="info-panel absolute bottom-0 left-0 right-0 px-4 py-3 backdrop-blur-md bg-fv-neutral-900/70 z-45"
|
|
627
699
|
>
|
|
628
|
-
|
|
629
|
-
<div v-if="paging" class="flex items-center justify-center pt-2">
|
|
630
|
-
<DefaultPaging :id="id" :items="paging" />
|
|
631
|
-
</div>
|
|
632
|
-
|
|
633
|
-
<!-- Thumbnail Grid -->
|
|
634
|
-
<div class="grid grid-cols-2 gap-2 p-2">
|
|
635
|
-
<div
|
|
636
|
-
v-for="i in images.length"
|
|
637
|
-
:key="`bg_${id}_${i}`"
|
|
638
|
-
class="group relative"
|
|
639
|
-
>
|
|
640
|
-
<div
|
|
641
|
-
class="absolute inset-0 rounded-lg transition-colors duration-300 group-hover:bg-fv-neutral-700/40"
|
|
642
|
-
:class="{ 'bg-fv-primary-500/40': i - 1 === modelValue }"
|
|
643
|
-
/>
|
|
644
|
-
<img
|
|
645
|
-
v-if="imageComponent === 'img'"
|
|
646
|
-
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow transition-all duration-300 group-hover:brightness-110 ${getBorderColor(
|
|
647
|
-
images[i - 1],
|
|
648
|
-
)}`"
|
|
649
|
-
:style="{
|
|
650
|
-
filter:
|
|
651
|
-
i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
652
|
-
}"
|
|
653
|
-
:src="getThumbnailUrl(images[i - 1])"
|
|
654
|
-
:alt="`Thumbnail ${i}`"
|
|
655
|
-
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
656
|
-
>
|
|
657
|
-
<component
|
|
658
|
-
:is="imageComponent"
|
|
659
|
-
v-else
|
|
660
|
-
:image="getThumbnailUrl(images[i - 1]).image"
|
|
661
|
-
:variant="getThumbnailUrl(images[i - 1]).variant"
|
|
662
|
-
:alt="getThumbnailUrl(images[i - 1]).alt"
|
|
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
|
-
:likes="getThumbnailUrl(images[i - 1]).likes"
|
|
671
|
-
:show-likes="getThumbnailUrl(images[i - 1]).showLikes"
|
|
672
|
-
:is-author="getThumbnailUrl(images[i - 1]).isAuthor"
|
|
673
|
-
:user-uuid="getThumbnailUrl(images[i - 1]).userUUID"
|
|
674
|
-
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
675
|
-
/>
|
|
676
|
-
</div>
|
|
677
|
-
</div>
|
|
700
|
+
<slot :value="images[modelValue]" />
|
|
678
701
|
</div>
|
|
679
702
|
</transition>
|
|
680
703
|
</div>
|
|
681
704
|
|
|
682
|
-
<!--
|
|
705
|
+
<!-- Side Thumbnails Panel -->
|
|
683
706
|
<transition
|
|
684
|
-
enter-active-class="transition-
|
|
685
|
-
enter-from-class="
|
|
686
|
-
enter-to-class="
|
|
687
|
-
leave-active-class="transition-
|
|
688
|
-
leave-from-class="
|
|
689
|
-
leave-to-class="
|
|
707
|
+
enter-active-class="transform transition ease-in-out duration-300"
|
|
708
|
+
enter-from-class="translate-x-full"
|
|
709
|
+
enter-to-class="translate-x-0"
|
|
710
|
+
leave-active-class="transform transition ease-in-out duration-300"
|
|
711
|
+
leave-from-class="translate-x-0"
|
|
712
|
+
leave-to-class="translate-x-full"
|
|
690
713
|
>
|
|
691
714
|
<div
|
|
692
|
-
v-if="
|
|
693
|
-
|
|
715
|
+
v-if="sidePanel"
|
|
716
|
+
ref="sidePanelRef"
|
|
717
|
+
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"
|
|
718
|
+
:style="{ 'padding-top': `${topControlsHeight}px` }"
|
|
694
719
|
>
|
|
695
|
-
<!--
|
|
696
|
-
<div class="flex items-center
|
|
697
|
-
<
|
|
698
|
-
<span class="text-sm opacity-80">{{ currentIndex }} / {{ imageCount }}</span>
|
|
720
|
+
<!-- Paging Controls if needed -->
|
|
721
|
+
<div v-if="paging" class="flex items-center justify-center pt-2">
|
|
722
|
+
<DefaultPaging :id="id" :items="paging" />
|
|
699
723
|
</div>
|
|
700
724
|
|
|
701
|
-
<!--
|
|
702
|
-
<div class="
|
|
703
|
-
<
|
|
704
|
-
|
|
705
|
-
:
|
|
706
|
-
|
|
707
|
-
@click="toggleInfoPanel"
|
|
708
|
-
>
|
|
709
|
-
<InformationCircleIcon class="w-5 h-5" />
|
|
710
|
-
</button>
|
|
711
|
-
|
|
712
|
-
<button
|
|
713
|
-
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"
|
|
714
|
-
:title="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
|
|
715
|
-
@click="() => (sidePanel = !sidePanel)"
|
|
716
|
-
>
|
|
717
|
-
<ChevronDoubleRightIcon v-if="sidePanel" class="w-5 h-5" />
|
|
718
|
-
<ChevronDoubleLeftIcon v-else class="w-5 h-5" />
|
|
719
|
-
</button>
|
|
720
|
-
|
|
721
|
-
<button
|
|
722
|
-
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"
|
|
723
|
-
:title="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
|
|
724
|
-
@click="() => (sidePanel = !sidePanel)"
|
|
725
|
-
>
|
|
726
|
-
<ChevronDoubleRightIcon v-if="sidePanel" class="w-5 h-5" />
|
|
727
|
-
<ChevronDoubleLeftIcon v-else class="w-5 h-5" />
|
|
728
|
-
</button>
|
|
729
|
-
|
|
730
|
-
<button
|
|
731
|
-
class="btn p-1.5 rounded-full bg-fv-neutral-800/70 hover:bg-fv-neutral-700/90 transition-transform transform hover:scale-110"
|
|
732
|
-
aria-label="Close gallery"
|
|
733
|
-
@click="setModal(false)"
|
|
725
|
+
<!-- Thumbnail Grid -->
|
|
726
|
+
<div class="grid grid-cols-2 gap-2 p-2">
|
|
727
|
+
<div
|
|
728
|
+
v-for="i in images.length"
|
|
729
|
+
:key="`bg_${id}_${i}`"
|
|
730
|
+
class="group relative"
|
|
734
731
|
>
|
|
735
|
-
<
|
|
736
|
-
|
|
732
|
+
<div
|
|
733
|
+
class="absolute inset-0 rounded-lg transition-colors duration-300 group-hover:bg-fv-neutral-700/40"
|
|
734
|
+
:class="{ 'bg-fv-primary-500/40': i - 1 === modelValue }"
|
|
735
|
+
/>
|
|
736
|
+
<img
|
|
737
|
+
v-if="imageComponent === 'img'"
|
|
738
|
+
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow transition-all duration-300 group-hover:brightness-110 ${getBorderColor(
|
|
739
|
+
images[i - 1],
|
|
740
|
+
)}`"
|
|
741
|
+
:style="{
|
|
742
|
+
filter:
|
|
743
|
+
i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
744
|
+
}"
|
|
745
|
+
:src="getThumbnailUrl(images[i - 1])"
|
|
746
|
+
:alt="`Thumbnail ${i}`"
|
|
747
|
+
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
748
|
+
>
|
|
749
|
+
<component
|
|
750
|
+
:is="imageComponent"
|
|
751
|
+
v-else
|
|
752
|
+
:image="getThumbnailUrl(images[i - 1]).image"
|
|
753
|
+
:variant="getThumbnailUrl(images[i - 1]).variant"
|
|
754
|
+
:alt="getThumbnailUrl(images[i - 1]).alt"
|
|
755
|
+
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow transition-all duration-300 group-hover:brightness-110 ${getBorderColor(
|
|
756
|
+
images[i - 1],
|
|
757
|
+
)}`"
|
|
758
|
+
:style="{
|
|
759
|
+
filter:
|
|
760
|
+
i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.7)',
|
|
761
|
+
}"
|
|
762
|
+
:likes="getThumbnailUrl(images[i - 1]).likes"
|
|
763
|
+
:show-likes="getThumbnailUrl(images[i - 1]).showLikes"
|
|
764
|
+
:is-author="getThumbnailUrl(images[i - 1]).isAuthor"
|
|
765
|
+
:user-uuid="getThumbnailUrl(images[i - 1]).userUUID"
|
|
766
|
+
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
767
|
+
/>
|
|
768
|
+
</div>
|
|
737
769
|
</div>
|
|
738
770
|
</div>
|
|
739
771
|
</transition>
|
|
740
772
|
|
|
741
|
-
<!-- Mobile Thumbnail Preview -->
|
|
773
|
+
<!-- Mobile Thumbnail Preview (bottom of screen on mobile) -->
|
|
742
774
|
<transition
|
|
743
775
|
enter-active-class="transition-transform duration-300 ease-out"
|
|
744
776
|
enter-from-class="translate-y-full"
|
|
@@ -749,7 +781,8 @@ onUnmounted(() => {
|
|
|
749
781
|
>
|
|
750
782
|
<div
|
|
751
783
|
v-if="showControls && images.length > 1 && !sidePanel"
|
|
752
|
-
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-
|
|
784
|
+
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-45"
|
|
785
|
+
:class="{ 'pb-20': infoPanel }"
|
|
753
786
|
>
|
|
754
787
|
<div class="overflow-x-auto flex space-x-2 pb-1 px-1">
|
|
755
788
|
<div
|
|
@@ -787,7 +820,7 @@ onUnmounted(() => {
|
|
|
787
820
|
</div>
|
|
788
821
|
</transition>
|
|
789
822
|
|
|
790
|
-
<!-- Thumbnail Grid/Mason/Custom Layouts -->
|
|
823
|
+
<!-- Thumbnail Grid/Mason/Custom Layouts for non-opened gallery -->
|
|
791
824
|
<div v-if="mode === 'grid' || mode === 'mason' || mode === 'custom'" class="gallery-grid">
|
|
792
825
|
<div
|
|
793
826
|
:class="{
|
|
@@ -881,6 +914,43 @@ onUnmounted(() => {
|
|
|
881
914
|
</template>
|
|
882
915
|
|
|
883
916
|
<style scoped>
|
|
917
|
+
/* Ensure controls stay fixed at top */
|
|
918
|
+
.controls-bar {
|
|
919
|
+
height: auto;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/* Layout container for main image and info panel */
|
|
923
|
+
.image-container {
|
|
924
|
+
position: relative;
|
|
925
|
+
display: flex;
|
|
926
|
+
flex-direction: column;
|
|
927
|
+
justify-content: center;
|
|
928
|
+
align-items: center;
|
|
929
|
+
height: 100%;
|
|
930
|
+
width: 100%;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/* Side panel positioning */
|
|
934
|
+
.side-panel {
|
|
935
|
+
height: 100vh;
|
|
936
|
+
overflow-y: auto;
|
|
937
|
+
overflow-x: hidden;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/* Info panel styling */
|
|
941
|
+
.info-panel {
|
|
942
|
+
width: 100%;
|
|
943
|
+
border-top-left-radius: 0.5rem;
|
|
944
|
+
border-top-right-radius: 0.5rem;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/* Image sizing in different contexts */
|
|
948
|
+
.image-display img,
|
|
949
|
+
.image-display .video-component {
|
|
950
|
+
transition: max-height 0.3s ease-out, max-width 0.3s ease-out;
|
|
951
|
+
object-fit: contain;
|
|
952
|
+
}
|
|
953
|
+
|
|
884
954
|
/* Transition styles for next (right) navigation */
|
|
885
955
|
.slide-next-enter-active,
|
|
886
956
|
.slide-next-leave-active {
|
|
@@ -892,8 +962,8 @@ onUnmounted(() => {
|
|
|
892
962
|
|
|
893
963
|
.slide-next-enter-from {
|
|
894
964
|
opacity: 0;
|
|
895
|
-
transform: translateX(
|
|
896
|
-
filter: blur(
|
|
965
|
+
transform: translateX(30px);
|
|
966
|
+
filter: blur(8px);
|
|
897
967
|
}
|
|
898
968
|
|
|
899
969
|
.slide-next-enter-to {
|
|
@@ -910,8 +980,8 @@ onUnmounted(() => {
|
|
|
910
980
|
|
|
911
981
|
.slide-next-leave-to {
|
|
912
982
|
opacity: 0;
|
|
913
|
-
transform: translateX(-
|
|
914
|
-
filter: blur(
|
|
983
|
+
transform: translateX(-30px);
|
|
984
|
+
filter: blur(8px);
|
|
915
985
|
}
|
|
916
986
|
|
|
917
987
|
/* Transition styles for prev (left) navigation */
|
|
@@ -925,8 +995,8 @@ onUnmounted(() => {
|
|
|
925
995
|
|
|
926
996
|
.slide-prev-enter-from {
|
|
927
997
|
opacity: 0;
|
|
928
|
-
transform: translateX(-
|
|
929
|
-
filter: blur(
|
|
998
|
+
transform: translateX(-30px);
|
|
999
|
+
filter: blur(8px);
|
|
930
1000
|
}
|
|
931
1001
|
|
|
932
1002
|
.slide-prev-enter-to {
|
|
@@ -943,11 +1013,11 @@ onUnmounted(() => {
|
|
|
943
1013
|
|
|
944
1014
|
.slide-prev-leave-to {
|
|
945
1015
|
opacity: 0;
|
|
946
|
-
transform: translateX(
|
|
947
|
-
filter: blur(
|
|
1016
|
+
transform: translateX(30px);
|
|
1017
|
+
filter: blur(8px);
|
|
948
1018
|
}
|
|
949
1019
|
|
|
950
|
-
/*
|
|
1020
|
+
/* Grid layouts for thumbnails */
|
|
951
1021
|
.gallery-grid {
|
|
952
1022
|
min-height: 200px;
|
|
953
1023
|
}
|