Mass changes

This commit is contained in:
Simon Pocrnjič
2025-10-04 23:36:18 +02:00
parent ab50336e97
commit fe91c7e4bc
46 changed files with 5738 additions and 1873 deletions
+336 -140
View File
@@ -1,198 +1,394 @@
<script setup>
import { FwbTable, FwbTableHead, FwbTableHeadCell, FwbTableBody, FwbTableRow, FwbTableCell, FwbBadge } from 'flowbite-vue'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { faFilePdf, faFileWord, faFileExcel, faFileLines, faFileImage, faFile, faCircleInfo, faEllipsisVertical, faDownload } from '@fortawesome/free-solid-svg-icons'
import { ref } from 'vue'
import Dropdown from '@/Components/Dropdown.vue'
import {
FwbTable,
FwbTableHead,
FwbTableHeadCell,
FwbTableBody,
FwbTableRow,
FwbTableCell,
FwbBadge,
} from "flowbite-vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import {
faFilePdf,
faFileWord,
faFileExcel,
faFileLines,
faFileImage,
faFile,
faCircleInfo,
faEllipsisVertical,
faDownload,
faTrash,
} from "@fortawesome/free-solid-svg-icons";
import { ref } from "vue";
import { router } from "@inertiajs/vue3";
import Dropdown from "@/Components/Dropdown.vue";
import ConfirmationModal from "@/Components/ConfirmationModal.vue";
import SecondaryButton from "./SecondaryButton.vue";
import DangerButton from "./DangerButton.vue";
const props = defineProps({
documents: { type: Array, default: () => [] },
viewUrlBuilder: { type: Function, default: null },
// Optional: build a direct download URL for a document; if not provided, a 'download' event will be emitted
// Optional: direct download URL builder; if absent we emit 'download'
downloadUrlBuilder: { type: Function, default: null },
})
// Optional: direct delete URL builder; if absent we emit 'delete'
deleteUrlBuilder: { type: Function, default: null },
});
// Derive a human-friendly source for a document: Case or Contract reference
const sourceLabel = (doc) => {
// Server can include optional documentable meta; fall back to type
if (doc.documentable_type?.toLowerCase?.().includes('contract')) {
return doc.contract_reference ? `Pogodba ${doc.contract_reference}` : 'Pogodba'
if (doc.documentable_type?.toLowerCase?.().includes("contract")) {
return doc.contract_reference ? `Pogodba ${doc.contract_reference}` : "Pogodba";
}
return 'Primer'
}
return "Primer";
};
const emit = defineEmits(['view', 'download'])
const emit = defineEmits(["view", "download", "delete"]);
const formatSize = (bytes) => {
if (bytes == null) return '-'
const thresh = 1024
if (Math.abs(bytes) < thresh) return bytes + ' B'
const units = ['KB', 'MB', 'GB', 'TB']
let u = -1
do { bytes /= thresh; ++u } while (Math.abs(bytes) >= thresh && u < units.length - 1)
return bytes.toFixed(1) + ' ' + units[u]
}
if (bytes == null) return "-";
const thresh = 1024;
if (Math.abs(bytes) < thresh) return bytes + " B";
const units = ["KB", "MB", "GB", "TB"];
let u = -1;
do {
bytes /= thresh;
++u;
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
return bytes.toFixed(1) + " " + units[u];
};
const extFrom = (doc) => {
let ext = (doc?.extension || '').toLowerCase()
let ext = (doc?.extension || "").toLowerCase();
if (!ext && doc?.original_name) {
const parts = String(doc.original_name).toLowerCase().split('.')
if (parts.length > 1) ext = parts.pop()
const parts = String(doc.original_name).toLowerCase().split(".");
if (parts.length > 1) ext = parts.pop();
}
// derive from mime
if (!ext && doc?.mime_type) {
const mime = String(doc.mime_type).toLowerCase()
if (mime.includes('pdf')) ext = 'pdf'
else if (mime.includes('word') || mime.includes('msword') || mime.includes('doc')) ext = 'docx'
else if (mime.includes('excel') || mime.includes('sheet')) ext = 'xlsx'
else if (mime.includes('csv')) ext = 'csv'
else if (mime.startsWith('image/')) ext = 'img'
else if (mime.includes('text')) ext = 'txt'
const mime = String(doc.mime_type).toLowerCase();
if (mime.includes("pdf")) ext = "pdf";
else if (mime.includes("word") || mime.includes("msword") || mime.includes("doc"))
ext = "docx";
else if (mime.includes("excel") || mime.includes("sheet")) ext = "xlsx";
else if (mime.includes("csv")) ext = "csv";
else if (mime.startsWith("image/")) ext = "img";
else if (mime.includes("text")) ext = "txt";
}
return ext
}
return ext;
};
const fileTypeInfo = (doc) => {
const ext = extFrom(doc)
const mime = (doc?.mime_type || '').toLowerCase()
const ext = extFrom(doc);
const mime = (doc?.mime_type || "").toLowerCase();
switch (ext) {
case 'pdf':
return { icon: faFilePdf, color: 'text-red-600', label: 'PDF' }
case 'doc':
case 'docx':
return { icon: faFileWord, color: 'text-blue-600', label: (ext || 'DOCX').toUpperCase() }
case 'xls':
case 'xlsx':
return { icon: faFileExcel, color: 'text-green-600', label: (ext || 'XLSX').toUpperCase() }
case 'csv':
case "pdf":
return { icon: faFilePdf, color: "text-red-600", label: "PDF" };
case "doc":
case "docx":
return {
icon: faFileWord,
color: "text-blue-600",
label: (ext || "DOCX").toUpperCase(),
};
case "xls":
case "xlsx":
return {
icon: faFileExcel,
color: "text-green-600",
label: (ext || "XLSX").toUpperCase(),
};
case "csv":
// treat CSV as spreadsheet-like
return { icon: faFileExcel, color: 'text-emerald-600', label: 'CSV' }
case 'txt':
return { icon: faFileLines, color: 'text-slate-600', label: 'TXT' }
case 'jpg':
case 'jpeg':
case 'png':
case 'img':
return { icon: faFileImage, color: 'text-fuchsia-600', label: (ext === 'img' ? 'IMG' : (ext || 'IMG').toUpperCase()) }
return { icon: faFileExcel, color: "text-emerald-600", label: "CSV" };
case "txt":
return { icon: faFileLines, color: "text-slate-600", label: "TXT" };
case "jpg":
case "jpeg":
case "png":
case "img":
return {
icon: faFileImage,
color: "text-fuchsia-600",
label: ext === "img" ? "IMG" : (ext || "IMG").toUpperCase(),
};
default:
if (mime.startsWith('image/')) return { icon: faFileImage, color: 'text-fuchsia-600', label: 'IMG' }
return { icon: faFile, color: 'text-gray-600', label: (ext || 'FILE').toUpperCase() }
if (mime.startsWith("image/"))
return { icon: faFileImage, color: "text-fuchsia-600", label: "IMG" };
return {
icon: faFile,
color: "text-gray-600",
label: (ext || "FILE").toUpperCase(),
};
}
}
};
const hasDesc = (doc) => {
const d = doc?.description
return typeof d === 'string' && d.trim().length > 0
}
const d = doc?.description;
return typeof d === "string" && d.trim().length > 0;
};
const expandedDescKey = ref(null)
const rowKey = (doc, i) => doc?.uuid ?? i
const expandedDescKey = ref(null);
const rowKey = (doc, i) => doc?.uuid ?? i;
const toggleDesc = (doc, i) => {
const key = rowKey(doc, i)
expandedDescKey.value = expandedDescKey.value === key ? null : key
}
const key = rowKey(doc, i);
expandedDescKey.value = expandedDescKey.value === key ? null : key;
};
const resolveDownloadUrl = (doc) => {
if (typeof props.downloadUrlBuilder === 'function') return props.downloadUrlBuilder(doc)
if (typeof props.downloadUrlBuilder === "function")
return props.downloadUrlBuilder(doc);
// If no builder provided, parent can handle via emitted event
return null
}
return null;
};
const handleDownload = (doc) => {
const url = resolveDownloadUrl(doc)
const url = resolveDownloadUrl(doc);
if (url) {
// Trigger a navigation to a direct-download endpoint; server should set Content-Disposition: attachment
const a = document.createElement('a')
a.href = url
a.target = '_self'
a.rel = 'noopener'
const a = document.createElement("a");
a.href = url;
a.target = "_self";
a.rel = "noopener";
// In many browsers, simply setting href is enough
a.click()
a.click();
} else {
emit('download', doc)
emit("download", doc);
}
closeActions()
closeActions();
};
// ---------------- Delete logic ----------------
const confirmDelete = ref(false);
const deleting = ref(false);
const docToDelete = ref(null);
const resolveDeleteUrl = (doc) => {
// 1. Explicit builder via prop takes precedence
if (typeof props.deleteUrlBuilder === "function") {
return props.deleteUrlBuilder(doc);
}
// 2. Attempt automatic route resolution (requires Ziggy's global `route` helper)
try {
const type = (doc?.documentable_type || "").toLowerCase();
// Contract document
if (type.includes("contract") && doc?.contract_uuid && doc?.uuid) {
if (typeof route === "function") {
return route("contract.document.delete", {
contract: doc.contract_uuid,
document: doc.uuid,
});
}
}
// Case document
if (doc?.client_case_uuid && doc?.uuid) {
if (typeof route === "function") {
return route("clientCase.document.delete", {
client_case: doc.client_case_uuid,
document: doc.uuid,
});
}
}
} catch (e) {
// swallow fallback to emit path
}
// 3. Fallback: no URL, caller must handle emitted event
return null;
};
const requestDelete = async () => {
if (!docToDelete.value) {
return;
}
const url = resolveDeleteUrl(docToDelete.value);
deleting.value = true;
try {
if (url) {
await router.delete(url, { preserveScroll: true });
} else {
emit("delete", docToDelete.value);
}
} finally {
deleting.value = false;
confirmDelete.value = false;
docToDelete.value = null;
}
};
const askDelete = (doc) => {
docToDelete.value = doc;
confirmDelete.value = true;
};
function closeActions() {
/* noop placeholder for symmetry; Dropdown auto-closes */
}
</script>
<template>
<div class="relative overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm">
<div
class="relative overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm"
>
<FwbTable hoverable striped class="text-sm">
<FwbTableHead class="sticky top-0 z-10 bg-gray-50/90 backdrop-blur border-b border-gray-200 shadow-sm">
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Naziv</FwbTableHeadCell>
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Vrsta</FwbTableHeadCell>
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Velikost</FwbTableHeadCell>
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Dodano</FwbTableHeadCell>
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Vir</FwbTableHeadCell>
<FwbTableHead
class="sticky top-0 z-10 bg-gray-50/90 backdrop-blur border-b border-gray-200 shadow-sm"
>
<FwbTableHeadCell
class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3"
>Naziv</FwbTableHeadCell
>
<FwbTableHeadCell
class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3"
>Vrsta</FwbTableHeadCell
>
<FwbTableHeadCell
class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3"
>Velikost</FwbTableHeadCell
>
<FwbTableHeadCell
class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3"
>Dodano</FwbTableHeadCell
>
<FwbTableHeadCell
class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3"
>Vir</FwbTableHeadCell
>
<FwbTableHeadCell class="w-px" />
</FwbTableHead>
<FwbTableBody>
<template v-for="(doc, i) in documents" :key="doc.uuid || i">
<FwbTableRow>
<FwbTableCell>
<div class="flex items-center gap-2">
<button type="button" class="text-indigo-600 hover:underline" @click="$emit('view', doc)">{{ doc.name }}</button>
<FwbBadge v-if="doc.is_public" type="green">Public</FwbBadge>
</div>
</FwbTableCell>
<FwbTableCell>
<div class="flex items-center gap-2">
<FontAwesomeIcon :icon="fileTypeInfo(doc).icon" :class="['h-5 w-5', fileTypeInfo(doc).color]" />
<span class="text-gray-700">{{ fileTypeInfo(doc).label }}</span>
</div>
</FwbTableCell>
<FwbTableCell>{{ formatSize(doc.size) }}</FwbTableCell>
<FwbTableCell>{{ new Date(doc.created_at).toLocaleString() }}</FwbTableCell>
<FwbTableCell>
<FwbBadge type="purple">{{ sourceLabel(doc) }}</FwbBadge>
</FwbTableCell>
<FwbTableCell class="text-center">
<button
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed"
:disabled="!hasDesc(doc)"
:title="hasDesc(doc) ? 'Pokaži opis' : 'Ni opisa'"
type="button"
@click="toggleDesc(doc, i)"
>
<FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4 text-gray-700" />
</button>
</FwbTableCell>
<FwbTableCell class="text-right whitespace-nowrap">
<Dropdown align="right" width="48">
<template #trigger>
<FwbTableRow>
<FwbTableCell>
<div class="flex items-center gap-2">
<button
type="button"
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 focus:outline-none"
:title="'Actions'"
class="text-indigo-600 hover:underline"
@click="$emit('view', doc)"
>
<FontAwesomeIcon :icon="faEllipsisVertical" class="h-4 w-4 text-gray-700" />
{{ doc.name }}
</button>
</template>
<template #content>
<button
type="button"
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
@click="handleDownload(doc)"
>
<FontAwesomeIcon :icon="faDownload" class="h-4 w-4 text-gray-600" />
<span>Download file</span>
</button>
<!-- future actions can be slotted here -->
</template>
</Dropdown>
</FwbTableCell>
</FwbTableRow>
<!-- Expanded description row directly below the item -->
<FwbTableRow :key="'desc-' + (doc.uuid || i)" v-if="expandedDescKey === rowKey(doc, i)">
<FwbTableCell :colspan="6" class="bg-gray-50">
<div class="px-4 py-3 text-sm text-gray-700 whitespace-pre-wrap border-l-2 border-indigo-400">
{{ doc.description }}
</div>
</FwbTableCell>
</FwbTableRow>
<FwbBadge v-if="doc.is_public" type="green">Public</FwbBadge>
</div>
</FwbTableCell>
<FwbTableCell>
<div class="flex items-center gap-2">
<FontAwesomeIcon
:icon="fileTypeInfo(doc).icon"
:class="['h-5 w-5', fileTypeInfo(doc).color]"
/>
<span class="text-gray-700">{{ fileTypeInfo(doc).label }}</span>
</div>
</FwbTableCell>
<FwbTableCell>{{ formatSize(doc.size) }}</FwbTableCell>
<FwbTableCell>{{ new Date(doc.created_at).toLocaleString() }}</FwbTableCell>
<FwbTableCell>
<FwbBadge type="purple">{{ sourceLabel(doc) }}</FwbBadge>
</FwbTableCell>
<FwbTableCell class="text-center">
<button
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed"
:disabled="!hasDesc(doc)"
:title="hasDesc(doc) ? 'Pokaži opis' : 'Ni opisa'"
type="button"
@click="toggleDesc(doc, i)"
>
<FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4 text-gray-700" />
</button>
</FwbTableCell>
<FwbTableCell class="text-right whitespace-nowrap">
<Dropdown align="right" width="48">
<template #trigger>
<button
type="button"
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 focus:outline-none"
:title="'Actions'"
>
<FontAwesomeIcon
:icon="faEllipsisVertical"
class="h-4 w-4 text-gray-700"
/>
</button>
</template>
<template #content>
<button
type="button"
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
@click="handleDownload(doc)"
>
<FontAwesomeIcon :icon="faDownload" class="h-4 w-4 text-gray-600" />
<span>Download file</span>
</button>
<button
type="button"
class="w-full px-3 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2"
@click="askDelete(doc)"
>
<FontAwesomeIcon :icon="faTrash" class="h-4 w-4" />
<span>Delete</span>
</button>
<!-- future actions can be slotted here -->
</template>
</Dropdown>
</FwbTableCell>
</FwbTableRow>
<!-- Expanded description row directly below the item -->
<FwbTableRow
:key="'desc-' + (doc.uuid || i)"
v-if="expandedDescKey === rowKey(doc, i)"
>
<FwbTableCell :colspan="6" class="bg-gray-50">
<div
class="px-4 py-3 text-sm text-gray-700 whitespace-pre-wrap border-l-2 border-indigo-400"
>
{{ doc.description }}
</div>
</FwbTableCell>
</FwbTableRow>
</template>
</FwbTableBody>
</FwbTable>
<div v-if="!documents || documents.length === 0" class="p-6 text-center text-sm text-gray-500">No documents.</div>
<div
v-if="!documents || documents.length === 0"
class="p-6 text-center text-sm text-gray-500"
>
No documents.
</div>
<!-- Delete confirmation modal using shared component -->
<ConfirmationModal
:show="confirmDelete"
:closeable="!deleting"
max-width="md"
@close="
confirmDelete = false;
docToDelete = null;
"
>
<template #title>Potrditev</template>
<template #content>
Ali res želite izbrisati dokument
<span class="font-medium">{{ docToDelete?.name }}</span
>? Tega dejanja ni mogoče razveljaviti.
</template>
<template #footer>
<SecondaryButton
type="button"
@click="
confirmDelete = false;
docToDelete = null;
"
:disabled="deleting"
>Prekliči</SecondaryButton
>
<DangerButton
:disabled="deleting"
type="button"
class="ml-2"
@click="requestDelete"
>{{ deleting ? "Brisanje…" : "Izbriši" }}</DangerButton
>
</template>
</ConfirmationModal>
</div>
</template>
+20 -4
View File
@@ -14,6 +14,10 @@ const props = defineProps({
type: Array,
default: () => ['py-1', 'bg-white'],
},
closeOnContentClick: {
type: Boolean,
default: true,
},
});
let open = ref(false);
@@ -77,9 +81,16 @@ onUnmounted(() => {
});
const widthClass = computed(() => {
return {
'48': 'w-48',
}[props.width.toString()];
const map = {
'48': 'w-48', // 12rem
'64': 'w-64', // 16rem
'72': 'w-72', // 18rem
'80': 'w-80', // 20rem
'96': 'w-96', // 24rem
'wide': 'w-[34rem] max-w-[90vw]',
'auto': '',
};
return map[props.width.toString()] ?? '';
});
const alignmentClasses = computed(() => {
@@ -93,6 +104,11 @@ const alignmentClasses = computed(() => {
return 'origin-top';
});
const onContentClick = () => {
if (props.closeOnContentClick) {
open.value = false;
}
};
</script>
<template>
@@ -120,7 +136,7 @@ const alignmentClasses = computed(() => {
:class="[widthClass]"
:style="[panelStyle]"
>
<div class="rounded-md ring-1 ring-black ring-opacity-5" :class="contentClasses" @click="open = false">
<div class="rounded-md ring-1 ring-black ring-opacity-5" :class="contentClasses" @click="onContentClick">
<slot name="content" />
</div>
</div>
+81 -70
View File
@@ -1,101 +1,112 @@
<script setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
const props = defineProps({
show: {
type: Boolean,
default: false,
},
maxWidth: {
type: String,
default: '2xl',
},
closeable: {
type: Boolean,
default: true,
},
show: {
type: Boolean,
default: false,
},
maxWidth: {
type: String,
default: "2xl",
},
closeable: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['close']);
const emit = defineEmits(["close"]);
const dialog = ref();
const showSlot = ref(props.show);
watch(() => props.show, () => {
watch(
() => props.show,
() => {
if (props.show) {
document.body.style.overflow = 'hidden';
showSlot.value = true;
dialog.value?.showModal();
document.body.style.overflow = "hidden";
showSlot.value = true;
dialog.value?.showModal();
} else {
document.body.style.overflow = null;
setTimeout(() => {
dialog.value?.close();
showSlot.value = false;
}, 200);
document.body.style.overflow = null;
setTimeout(() => {
dialog.value?.close();
showSlot.value = false;
}, 200);
}
});
}
);
const close = () => {
if (props.closeable) {
emit('close');
}
if (props.closeable) {
emit("close");
}
};
const closeOnEscape = (e) => {
if (e.key === 'Escape') {
e.preventDefault();
if (e.key === "Escape") {
e.preventDefault();
if (props.show) {
close();
}
if (props.show) {
close();
}
}
};
onMounted(() => document.addEventListener('keydown', closeOnEscape));
onMounted(() => document.addEventListener("keydown", closeOnEscape));
onUnmounted(() => {
document.removeEventListener('keydown', closeOnEscape);
document.body.style.overflow = null;
document.removeEventListener("keydown", closeOnEscape);
document.body.style.overflow = null;
});
const maxWidthClass = computed(() => {
return {
'sm': 'sm:max-w-sm',
'md': 'sm:max-w-md',
'lg': 'sm:max-w-lg',
'xl': 'sm:max-w-xl',
'2xl': 'sm:max-w-2xl',
}[props.maxWidth];
return {
sm: "sm:max-w-sm",
md: "sm:max-w-md",
lg: "sm:max-w-lg",
xl: "sm:max-w-xl",
"2xl": "sm:max-w-2xl",
wide: "sm:max-w-[1200px] w-full",
}[props.maxWidth];
});
</script>
<template>
<dialog class="z-50 m-0 min-h-full min-w-full overflow-y-auto bg-transparent backdrop:bg-transparent" ref="dialog">
<div class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50" scroll-region>
<transition
enter-active-class="ease-out duration-300"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="ease-in duration-200"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-show="show" class="fixed inset-0 transform transition-all" @click="close">
<div class="absolute inset-0 bg-gray-500 opacity-75" />
</div>
</transition>
<transition
enter-active-class="ease-out duration-300"
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
leave-active-class="ease-in duration-200"
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div v-show="show" class="mb-6 bg-white rounded-lg overflow-visible shadow-xl transform transition-all sm:w-full sm:mx-auto" :class="maxWidthClass">
<slot v-if="showSlot"/>
</div>
</transition>
<dialog
class="z-50 m-0 min-h-full min-w-full overflow-y-auto bg-transparent backdrop:bg-transparent"
ref="dialog"
>
<div class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50" scroll-region>
<transition
enter-active-class="ease-out duration-300"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="ease-in duration-200"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-show="show" class="fixed inset-0 transform transition-all" @click="close">
<div class="absolute inset-0 bg-gray-500 opacity-75" />
</div>
</dialog>
</transition>
<transition
enter-active-class="ease-out duration-300"
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
leave-active-class="ease-in duration-200"
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div
v-show="show"
class="mb-6 bg-white rounded-lg overflow-visible shadow-xl transform transition-all sm:w-full sm:mx-auto"
:class="maxWidthClass"
>
<slot v-if="showSlot" />
</div>
</transition>
</div>
</dialog>
</template>