@fy-/fws-vue 2.2.45 → 2.2.46
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.
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { onMounted, onUnmounted } from 'vue'
|
|
3
|
-
|
|
2
|
+
import { onMounted, onUnmounted, ref } from 'vue'
|
|
4
3
|
import ScaleTransition from './transitions/ScaleTransition.vue'
|
|
5
4
|
|
|
6
5
|
const props = defineProps<{
|
|
@@ -17,6 +16,17 @@ const props = defineProps<{
|
|
|
17
16
|
closeDropdown: () => void
|
|
18
17
|
}>()
|
|
19
18
|
|
|
19
|
+
const dropdownRef = ref<HTMLElement | null>(null)
|
|
20
|
+
|
|
21
|
+
// Custom implementation of click-outside functionality
|
|
22
|
+
function handleClickOutsideElement(event: MouseEvent) {
|
|
23
|
+
if (props.preventClickOutside) return
|
|
24
|
+
|
|
25
|
+
if (props.show && dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
|
|
26
|
+
props.handleClickOutside()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
function handleCloseOnEscape(event: KeyboardEvent) {
|
|
21
31
|
if (['Escape', 'Esc'].includes(event.key)) {
|
|
22
32
|
props.closeDropdown()
|
|
@@ -25,10 +35,12 @@ function handleCloseOnEscape(event: KeyboardEvent) {
|
|
|
25
35
|
|
|
26
36
|
onMounted(() => {
|
|
27
37
|
document.addEventListener('keydown', handleCloseOnEscape)
|
|
38
|
+
document.addEventListener('click', handleClickOutsideElement)
|
|
28
39
|
})
|
|
29
40
|
|
|
30
41
|
onUnmounted(() => {
|
|
31
42
|
document.removeEventListener('keydown', handleCloseOnEscape)
|
|
43
|
+
document.removeEventListener('click', handleClickOutsideElement)
|
|
32
44
|
})
|
|
33
45
|
</script>
|
|
34
46
|
|
|
@@ -42,7 +54,7 @@ onUnmounted(() => {
|
|
|
42
54
|
<ScaleTransition>
|
|
43
55
|
<div
|
|
44
56
|
v-show="props.show"
|
|
45
|
-
|
|
57
|
+
ref="dropdownRef"
|
|
46
58
|
:class="props.position"
|
|
47
59
|
:style="props.coordinates"
|
|
48
60
|
class="absolute z-[100] w-[200px] mt-2 rounded-sm bg-white dark:bg-fv-neutral-900 shadow-lg border border-fv-neutral-100 dark:border-fv-neutral-600 focus:outline-none"
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Component } from 'vue'
|
|
3
3
|
import type { APIPaging } from '../../composables/rest'
|
|
4
|
-
import { Dialog, DialogPanel, TransitionRoot } from '@headlessui/vue'
|
|
5
4
|
import {
|
|
6
5
|
ArrowLeftCircleIcon,
|
|
7
6
|
ArrowRightCircleIcon,
|
|
@@ -69,9 +68,19 @@ const direction = ref<'next' | 'prev'>('next')
|
|
|
69
68
|
function setModal(value: boolean) {
|
|
70
69
|
if (value === true) {
|
|
71
70
|
if (props.onOpen) props.onOpen()
|
|
71
|
+
document.body.style.overflow = 'hidden' // Prevent scrolling when gallery is open
|
|
72
|
+
if (!import.meta.env.SSR) {
|
|
73
|
+
document.addEventListener('keydown', handleKeyboardInput)
|
|
74
|
+
document.addEventListener('keyup', handleKeyboardRelease)
|
|
75
|
+
}
|
|
72
76
|
}
|
|
73
77
|
else {
|
|
74
78
|
if (props.onClose) props.onClose()
|
|
79
|
+
document.body.style.overflow = '' // Restore scrolling
|
|
80
|
+
if (!import.meta.env.SSR) {
|
|
81
|
+
document.removeEventListener('keydown', handleKeyboardInput)
|
|
82
|
+
document.removeEventListener('keyup', handleKeyboardRelease)
|
|
83
|
+
}
|
|
75
84
|
}
|
|
76
85
|
isGalleryOpen.value = value
|
|
77
86
|
}
|
|
@@ -164,8 +173,14 @@ function getBorderColor(i: any) {
|
|
|
164
173
|
const isKeyPressed = ref<boolean>(false)
|
|
165
174
|
|
|
166
175
|
function handleKeyboardInput(event: KeyboardEvent) {
|
|
176
|
+
if (!isGalleryOpen.value) return
|
|
167
177
|
if (isKeyPressed.value) return
|
|
178
|
+
|
|
168
179
|
switch (event.key) {
|
|
180
|
+
case 'Escape':
|
|
181
|
+
event.preventDefault()
|
|
182
|
+
setModal(false)
|
|
183
|
+
break
|
|
169
184
|
case 'ArrowRight':
|
|
170
185
|
isKeyPressed.value = true
|
|
171
186
|
direction.value = 'next'
|
|
@@ -191,54 +206,60 @@ function closeGallery() {
|
|
|
191
206
|
setModal(false)
|
|
192
207
|
}
|
|
193
208
|
|
|
209
|
+
// Click outside gallery content to close
|
|
210
|
+
function handleBackdropClick(event: MouseEvent) {
|
|
211
|
+
if (event.target === event.currentTarget) {
|
|
212
|
+
setModal(false)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
194
216
|
onMounted(() => {
|
|
195
217
|
eventBus.on(`${props.id}GalleryImage`, openGalleryImage)
|
|
196
218
|
eventBus.on(`${props.id}Gallery`, openGalleryImage)
|
|
197
219
|
eventBus.on(`${props.id}GalleryClose`, closeGallery)
|
|
198
|
-
if (window !== undefined && !import.meta.env.SSR) {
|
|
199
|
-
window.addEventListener('keydown', handleKeyboardInput)
|
|
200
|
-
window.addEventListener('keyup', handleKeyboardRelease)
|
|
201
|
-
}
|
|
202
220
|
})
|
|
203
221
|
|
|
204
222
|
onUnmounted(() => {
|
|
205
223
|
eventBus.off(`${props.id}Gallery`, openGalleryImage)
|
|
206
224
|
eventBus.off(`${props.id}GalleryImage`, openGalleryImage)
|
|
207
225
|
eventBus.off(`${props.id}GalleryClose`, closeGallery)
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
226
|
+
if (!import.meta.env.SSR) {
|
|
227
|
+
document.removeEventListener('keydown', handleKeyboardInput)
|
|
228
|
+
document.removeEventListener('keyup', handleKeyboardRelease)
|
|
229
|
+
document.body.style.overflow = '' // Ensure body scrolling is restored
|
|
211
230
|
}
|
|
212
231
|
})
|
|
213
232
|
</script>
|
|
214
233
|
|
|
215
234
|
<template>
|
|
216
235
|
<div>
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
enter="
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
leave="
|
|
224
|
-
leave-from="opacity-100"
|
|
225
|
-
leave-to="opacity-0"
|
|
236
|
+
<transition
|
|
237
|
+
enter-active-class="duration-300 ease-out"
|
|
238
|
+
enter-from-class="opacity-0"
|
|
239
|
+
enter-to-class="opacity-100"
|
|
240
|
+
leave-active-class="duration-200 ease-in"
|
|
241
|
+
leave-from-class="opacity-100"
|
|
242
|
+
leave-to-class="opacity-0"
|
|
226
243
|
>
|
|
227
|
-
<
|
|
228
|
-
|
|
244
|
+
<div
|
|
245
|
+
v-if="isGalleryOpen"
|
|
229
246
|
class="fixed bg-fv-neutral-900 text-white inset-0 max-w-[100vw] overflow-y-auto overflow-x-hidden"
|
|
230
247
|
style="z-index: 37"
|
|
231
|
-
|
|
248
|
+
role="dialog"
|
|
249
|
+
aria-modal="true"
|
|
250
|
+
@click="handleBackdropClick"
|
|
232
251
|
>
|
|
233
|
-
<
|
|
252
|
+
<div
|
|
234
253
|
class="relative w-full max-w-full flex flex-col justify-center items-center"
|
|
235
254
|
style="z-index: 38"
|
|
255
|
+
@click.stop
|
|
236
256
|
>
|
|
237
257
|
<div class="flex flex-grow gap-4 w-full max-w-full">
|
|
238
258
|
<div class="flex-grow h-[100vh] flex items-center relative">
|
|
239
259
|
<button
|
|
240
260
|
class="btn w-9 h-9 rounded-full absolute top-4 left-2"
|
|
241
261
|
style="z-index: 39"
|
|
262
|
+
aria-label="Close gallery"
|
|
242
263
|
@click="setModal(false)"
|
|
243
264
|
>
|
|
244
265
|
<component :is="closeIcon" class="w-8 h-8" />
|
|
@@ -253,6 +274,7 @@ onUnmounted(() => {
|
|
|
253
274
|
<button
|
|
254
275
|
v-if="images.length > 1"
|
|
255
276
|
class="btn p-1 rounded-full"
|
|
277
|
+
aria-label="Previous image"
|
|
256
278
|
@click="goPrevImage()"
|
|
257
279
|
>
|
|
258
280
|
<ArrowLeftCircleIcon class="w-8 h-8" />
|
|
@@ -291,6 +313,7 @@ onUnmounted(() => {
|
|
|
291
313
|
v-if="modelValueSrc && imageComponent === 'img'"
|
|
292
314
|
class="shadow max-w-full h-auto object-contain max-h-[85vh]"
|
|
293
315
|
:src="modelValueSrc"
|
|
316
|
+
:alt="`Gallery image ${modelValue + 1}`"
|
|
294
317
|
>
|
|
295
318
|
<component
|
|
296
319
|
:is="imageComponent"
|
|
@@ -320,6 +343,7 @@ onUnmounted(() => {
|
|
|
320
343
|
'right-2': !sidePanel,
|
|
321
344
|
}"
|
|
322
345
|
style="z-index: 39"
|
|
346
|
+
:aria-label="sidePanel ? 'Hide thumbnails' : 'Show thumbnails'"
|
|
323
347
|
@click="() => (sidePanel = !sidePanel)"
|
|
324
348
|
>
|
|
325
349
|
<ChevronDoubleRightIcon v-if="sidePanel" class="w-7 h-7" />
|
|
@@ -328,6 +352,7 @@ onUnmounted(() => {
|
|
|
328
352
|
<button
|
|
329
353
|
v-if="images.length > 1"
|
|
330
354
|
class="btn p-1 rounded-full"
|
|
355
|
+
aria-label="Next image"
|
|
331
356
|
@click="goNextImage()"
|
|
332
357
|
>
|
|
333
358
|
<ArrowRightCircleIcon class="w-8 h-8" />
|
|
@@ -336,57 +361,60 @@ onUnmounted(() => {
|
|
|
336
361
|
</div>
|
|
337
362
|
</div>
|
|
338
363
|
|
|
339
|
-
<
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
enter="
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
leave="
|
|
346
|
-
leave-from="translate-x-0"
|
|
347
|
-
leave-to="translate-x-full"
|
|
348
|
-
class="hidden lg:block flex-shrink-0 w-64 bg-fv-neutral-800 h-[100vh] max-h-[100vh] overflow-y-auto"
|
|
364
|
+
<transition
|
|
365
|
+
enter-active-class="transform transition ease-in-out duration-300"
|
|
366
|
+
enter-from-class="translate-x-full"
|
|
367
|
+
enter-to-class="translate-x-0"
|
|
368
|
+
leave-active-class="transform transition ease-in-out duration-300"
|
|
369
|
+
leave-from-class="translate-x-0"
|
|
370
|
+
leave-to-class="translate-x-full"
|
|
349
371
|
>
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
<div
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
images[i - 1],
|
|
368
|
-
)}`"
|
|
369
|
-
:src="getThumbnailUrl(images[i - 1])"
|
|
370
|
-
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
372
|
+
<div
|
|
373
|
+
v-if="sidePanel"
|
|
374
|
+
class="hidden lg:block flex-shrink-0 w-64 bg-fv-neutral-800 h-[100vh] max-h-[100vh] overflow-y-auto"
|
|
375
|
+
>
|
|
376
|
+
<!-- Side panel content -->
|
|
377
|
+
<div v-if="paging" class="flex items-center justify-center">
|
|
378
|
+
<DefaultPaging :id="id" :items="paging" />
|
|
379
|
+
</div>
|
|
380
|
+
<div class="grid grid-cols-2 gap-2 p-2">
|
|
381
|
+
<div
|
|
382
|
+
v-for="i in images.length"
|
|
383
|
+
:key="`bg_${id}_${i}`"
|
|
384
|
+
class="hover:!brightness-100"
|
|
385
|
+
:style="{
|
|
386
|
+
filter:
|
|
387
|
+
i - 1 === modelValue ? 'brightness(1)' : 'brightness(0.5)',
|
|
388
|
+
}"
|
|
371
389
|
>
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
390
|
+
<img
|
|
391
|
+
v-if="imageComponent === 'img'"
|
|
392
|
+
:class="`h-auto max-w-full rounded-lg cursor-pointer shadow ${getBorderColor(
|
|
393
|
+
images[i - 1],
|
|
394
|
+
)}`"
|
|
395
|
+
:src="getThumbnailUrl(images[i - 1])"
|
|
396
|
+
:alt="`Thumbnail ${i}`"
|
|
397
|
+
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
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
|
+
/>
|
|
410
|
+
</div>
|
|
383
411
|
</div>
|
|
384
412
|
</div>
|
|
385
|
-
</
|
|
413
|
+
</transition>
|
|
386
414
|
</div>
|
|
387
|
-
</
|
|
388
|
-
</
|
|
389
|
-
</
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
</transition>
|
|
390
418
|
|
|
391
419
|
<div v-if="mode === 'grid' || mode === 'mason' || mode === 'custom'" class="min-h-[600px]">
|
|
392
420
|
<div
|
|
@@ -415,6 +443,7 @@ onUnmounted(() => {
|
|
|
415
443
|
v-if="i + j - 2 < images.length && imageComponent === 'img'"
|
|
416
444
|
class="h-auto max-w-full rounded-lg cursor-pointer"
|
|
417
445
|
:src="getThumbnailUrl(images[i + j - 2])"
|
|
446
|
+
:alt="`Gallery image ${i + j - 1}`"
|
|
418
447
|
@click="$eventBus.emit(`${id}GalleryImage`, i + j - 2)"
|
|
419
448
|
>
|
|
420
449
|
<component
|
|
@@ -444,6 +473,7 @@ onUnmounted(() => {
|
|
|
444
473
|
v-if="imageComponent === 'img'"
|
|
445
474
|
class="h-auto max-w-full rounded-lg cursor-pointer"
|
|
446
475
|
:src="getThumbnailUrl(images[i - 1])"
|
|
476
|
+
:alt="`Gallery image ${i}`"
|
|
447
477
|
@click="$eventBus.emit(`${id}GalleryImage`, i - 1)"
|
|
448
478
|
>
|
|
449
479
|
<component
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
Dialog,
|
|
4
|
-
DialogPanel,
|
|
5
|
-
TransitionRoot,
|
|
6
|
-
} from '@headlessui/vue'
|
|
7
2
|
import { XCircleIcon } from '@heroicons/vue/24/solid'
|
|
8
|
-
import { h, onMounted, onUnmounted, ref } from 'vue'
|
|
3
|
+
import { h, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
9
4
|
import { useEventBus } from '../../composables/event-bus'
|
|
10
5
|
|
|
11
6
|
const props = withDefaults(
|
|
@@ -30,14 +25,55 @@ const eventBus = useEventBus()
|
|
|
30
25
|
const isOpen = ref<boolean>(false)
|
|
31
26
|
const modalRef = ref<HTMLElement | null>(null)
|
|
32
27
|
let previouslyFocusedElement: HTMLElement | null = null
|
|
28
|
+
let focusableElements: HTMLElement[] = []
|
|
29
|
+
|
|
30
|
+
// Trap focus within modal for accessibility
|
|
31
|
+
function getFocusableElements(element: HTMLElement): HTMLElement[] {
|
|
32
|
+
return Array.from(
|
|
33
|
+
element.querySelectorAll(
|
|
34
|
+
'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])',
|
|
35
|
+
),
|
|
36
|
+
).filter(
|
|
37
|
+
el => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden'),
|
|
38
|
+
) as HTMLElement[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
42
|
+
if (!isOpen.value) return
|
|
43
|
+
|
|
44
|
+
// Close on escape
|
|
45
|
+
if (event.key === 'Escape') {
|
|
46
|
+
event.preventDefault()
|
|
47
|
+
setModal(false)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle tab trapping
|
|
52
|
+
if (event.key === 'Tab' && focusableElements.length > 0) {
|
|
53
|
+
// If shift + tab on first element, focus last element
|
|
54
|
+
if (event.shiftKey && document.activeElement === focusableElements[0]) {
|
|
55
|
+
event.preventDefault()
|
|
56
|
+
focusableElements[focusableElements.length - 1].focus()
|
|
57
|
+
}
|
|
58
|
+
// If tab on last element, focus first element
|
|
59
|
+
else if (!event.shiftKey && document.activeElement === focusableElements[focusableElements.length - 1]) {
|
|
60
|
+
event.preventDefault()
|
|
61
|
+
focusableElements[0].focus()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
33
65
|
|
|
34
66
|
function setModal(value: boolean) {
|
|
35
67
|
if (value === true) {
|
|
36
68
|
if (props.onOpen) props.onOpen()
|
|
37
69
|
previouslyFocusedElement = document.activeElement as HTMLElement
|
|
70
|
+
document.body.style.overflow = 'hidden' // Prevent scrolling when modal is open
|
|
71
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
38
72
|
}
|
|
39
73
|
if (value === false) {
|
|
40
74
|
if (props.onClose) props.onClose()
|
|
75
|
+
document.body.style.overflow = '' // Restore scrolling
|
|
76
|
+
document.removeEventListener('keydown', handleKeyDown)
|
|
41
77
|
if (previouslyFocusedElement) {
|
|
42
78
|
previouslyFocusedElement.focus()
|
|
43
79
|
}
|
|
@@ -45,54 +81,80 @@ function setModal(value: boolean) {
|
|
|
45
81
|
isOpen.value = value
|
|
46
82
|
}
|
|
47
83
|
|
|
84
|
+
// After modal is opened, set focus and collect focusable elements
|
|
85
|
+
watch(isOpen, async (newVal) => {
|
|
86
|
+
if (newVal) {
|
|
87
|
+
await nextTick()
|
|
88
|
+
if (modalRef.value) {
|
|
89
|
+
focusableElements = getFocusableElements(modalRef.value)
|
|
90
|
+
|
|
91
|
+
// Focus the first focusable element or the close button if available
|
|
92
|
+
const closeButton = modalRef.value.querySelector('button[aria-label="Close modal"]') as HTMLElement
|
|
93
|
+
if (closeButton) {
|
|
94
|
+
closeButton.focus()
|
|
95
|
+
}
|
|
96
|
+
else if (focusableElements.length > 0) {
|
|
97
|
+
focusableElements[0].focus()
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// If no focusable elements, focus the modal itself
|
|
101
|
+
modalRef.value.focus()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
48
107
|
onMounted(() => {
|
|
49
108
|
eventBus.on(`${props.id}Modal`, setModal)
|
|
50
109
|
})
|
|
51
110
|
|
|
52
111
|
onUnmounted(() => {
|
|
53
112
|
eventBus.off(`${props.id}Modal`, setModal)
|
|
113
|
+
document.removeEventListener('keydown', handleKeyDown)
|
|
114
|
+
document.body.style.overflow = '' // Ensure body scrolling is restored
|
|
54
115
|
})
|
|
55
116
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if
|
|
59
|
-
|
|
60
|
-
|
|
117
|
+
// Click outside to close
|
|
118
|
+
function handleBackdropClick(event: MouseEvent) {
|
|
119
|
+
// Close only if clicking the backdrop, not the modal content
|
|
120
|
+
if (event.target === event.currentTarget) {
|
|
121
|
+
setModal(false)
|
|
61
122
|
}
|
|
62
|
-
}
|
|
63
|
-
*/
|
|
123
|
+
}
|
|
64
124
|
</script>
|
|
65
125
|
|
|
66
126
|
<template>
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
enter="
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
leave="
|
|
74
|
-
leave-from="opacity-100"
|
|
75
|
-
leave-to="opacity-0"
|
|
127
|
+
<transition
|
|
128
|
+
enter-active-class="duration-300 ease-out"
|
|
129
|
+
enter-from-class="opacity-0"
|
|
130
|
+
enter-to-class="opacity-100"
|
|
131
|
+
leave-active-class="duration-200 ease-in"
|
|
132
|
+
leave-from-class="opacity-100"
|
|
133
|
+
leave-to-class="opacity-0"
|
|
76
134
|
>
|
|
77
|
-
<
|
|
78
|
-
|
|
135
|
+
<div
|
|
136
|
+
v-if="isOpen"
|
|
79
137
|
class="fixed inset-0 overflow-y-auto"
|
|
80
138
|
style="z-index: 40"
|
|
81
|
-
aria-modal="true"
|
|
82
139
|
role="dialog"
|
|
83
140
|
:aria-labelledby="title ? `${props.id}-title` : undefined"
|
|
84
|
-
|
|
141
|
+
aria-modal="true"
|
|
85
142
|
>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
tabindex="-1"
|
|
143
|
+
<!-- Backdrop with click to close functionality -->
|
|
144
|
+
<div
|
|
89
145
|
class="flex absolute backdrop-blur-[8px] inset-0 flex-col items-center justify-center min-h-screen text-fv-neutral-800 dark:text-fv-neutral-300 bg-fv-neutral-900/[.20] dark:bg-fv-neutral-50/[.20]"
|
|
90
146
|
style="z-index: 41"
|
|
147
|
+
@click="handleBackdropClick"
|
|
91
148
|
>
|
|
149
|
+
<!-- Modal panel -->
|
|
92
150
|
<div
|
|
151
|
+
ref="modalRef"
|
|
93
152
|
:class="`relative ${mSize} max-w-6xl max-h-full ${ofy} bg-white rounded-lg shadow dark:bg-fv-neutral-900`"
|
|
94
153
|
style="z-index: 42"
|
|
154
|
+
tabindex="-1"
|
|
155
|
+
@click.stop
|
|
95
156
|
>
|
|
157
|
+
<!-- Header with title if provided -->
|
|
96
158
|
<div
|
|
97
159
|
v-if="title"
|
|
98
160
|
class="flex items-center justify-between p-2 w-full border-b rounded-t dark:border-fv-neutral-700"
|
|
@@ -112,11 +174,12 @@ watch(isOpen, async (newVal) => {
|
|
|
112
174
|
<component :is="closeIcon" class="w-7 h-7" />
|
|
113
175
|
</button>
|
|
114
176
|
</div>
|
|
177
|
+
<!-- Content area -->
|
|
115
178
|
<div class="p-3 space-y-3">
|
|
116
179
|
<slot />
|
|
117
180
|
</div>
|
|
118
181
|
</div>
|
|
119
|
-
</
|
|
120
|
-
</
|
|
121
|
-
</
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</transition>
|
|
122
185
|
</template>
|