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