- Replace card-based agenda layout with a 6-column table (Nr, Zeit, Dauer, Titel, Verantwortlich, Aktionen)
- Fix isHeaderType to only match type=header, not all non-song items
- Format start time from ISO 8601 to HH:MM (Europe/Berlin)
- Format duration from seconds string to H:MM
- Fix responsible parsing for CTS data structure ({text, persons[{person:{title}}]})
- Move unmatched song search/assign UI into ArrangementDialog popup
- Add colored row backgrounds (song/sermon/announcement/slides) with left border
- Invert header rows to dark grey background with white text
643 lines
26 KiB
Vue
643 lines
26 KiB
Vue
<script setup>
|
||
import { computed, nextTick, ref, watch, onMounted, onUnmounted } from 'vue'
|
||
import { router } from '@inertiajs/vue3'
|
||
import { VueDraggable } from 'vue-draggable-plus'
|
||
|
||
const props = defineProps({
|
||
songId: {
|
||
type: Number,
|
||
default: null,
|
||
},
|
||
arrangements: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
availableGroups: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
selectedArrangementId: {
|
||
type: Number,
|
||
default: null,
|
||
},
|
||
serviceSongId: {
|
||
type: Number,
|
||
default: null,
|
||
},
|
||
songsCatalog: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
})
|
||
|
||
const emit = defineEmits(['close', 'arrangement-selected'])
|
||
|
||
/* ── Song assignment (unmatched songs) ── */
|
||
|
||
const isUnmatched = computed(() => !props.songId)
|
||
|
||
const searchQuery = ref('')
|
||
const selectedSongId = ref('')
|
||
const dropdownOpen = ref(false)
|
||
const assignError = ref('')
|
||
|
||
function normalize(value) {
|
||
return (value ?? '').toString().toLowerCase().trim()
|
||
}
|
||
|
||
const filteredCatalog = computed(() => {
|
||
const term = normalize(searchQuery.value)
|
||
if (term === '') return props.songsCatalog.slice(0, 100)
|
||
return props.songsCatalog
|
||
.filter((song) => normalize(song.title).includes(term) || normalize(song.ccli_id).includes(term))
|
||
.slice(0, 100)
|
||
})
|
||
|
||
function openSearchDropdown() {
|
||
dropdownOpen.value = true
|
||
}
|
||
|
||
function closeSearchDropdown() {
|
||
setTimeout(() => {
|
||
dropdownOpen.value = false
|
||
}, 200)
|
||
}
|
||
|
||
function selectSong(song) {
|
||
selectedSongId.value = song.id
|
||
searchQuery.value = song.title
|
||
dropdownOpen.value = false
|
||
}
|
||
|
||
async function assignSong() {
|
||
const songId = Number(selectedSongId.value)
|
||
if (!songId) {
|
||
assignError.value = 'Bitte wähle zuerst einen Song aus.'
|
||
return
|
||
}
|
||
assignError.value = ''
|
||
|
||
try {
|
||
await window.axios.post(`/api/service-songs/${props.serviceSongId}/assign`, {
|
||
song_id: songId,
|
||
})
|
||
emit('close')
|
||
} catch {
|
||
assignError.value = 'Zuordnung fehlgeschlagen.'
|
||
}
|
||
}
|
||
|
||
/* ── State ── */
|
||
|
||
const currentArrangementId = ref(
|
||
props.selectedArrangementId ?? props.arrangements.find((a) => a.is_default)?.id ?? props.arrangements[0]?.id ?? null,
|
||
)
|
||
|
||
const currentArrangement = computed(() =>
|
||
props.arrangements.find((a) => a.id === Number(currentArrangementId.value)) ?? null,
|
||
)
|
||
|
||
const arrangementGroups = ref([])
|
||
|
||
watch(
|
||
currentArrangementId,
|
||
(id) => {
|
||
const arr = props.arrangements.find((a) => a.id === Number(id))
|
||
if (arr?.groups?.length) {
|
||
arrangementGroups.value = arr.groups.map((g, i) => ({ ...g, _uid: `${g.id}-${i}-${Date.now()}` }))
|
||
} else {
|
||
// Fallback: show all available groups in order (Master)
|
||
arrangementGroups.value = props.availableGroups.map((g, i) => ({ ...g, _uid: `${g.id}-master-${i}-${Date.now()}` }))
|
||
}
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
|
||
const pendingAutoSelect = ref(false)
|
||
|
||
watch(
|
||
() => props.arrangements.length,
|
||
(newLen, oldLen) => {
|
||
if (pendingAutoSelect.value && newLen > oldLen) {
|
||
const newest = props.arrangements.reduce((a, b) => (a.id > b.id ? a : b))
|
||
currentArrangementId.value = newest.id
|
||
pendingAutoSelect.value = false
|
||
}
|
||
},
|
||
)
|
||
|
||
/* ── Scroll sync ── */
|
||
|
||
const leftCol = ref(null)
|
||
const rightCol = ref(null)
|
||
let syncing = false
|
||
|
||
function syncScroll(from) {
|
||
if (syncing) return
|
||
syncing = true
|
||
if (from === 'left' && rightCol.value) {
|
||
rightCol.value.scrollTop = leftCol.value.scrollTop
|
||
} else if (from === 'right' && leftCol.value) {
|
||
leftCol.value.scrollTop = rightCol.value.scrollTop
|
||
}
|
||
nextTick(() => {
|
||
syncing = false
|
||
})
|
||
}
|
||
|
||
/* ── Group picker ── */
|
||
|
||
const groupPickerOpen = ref(null)
|
||
|
||
function toggleGroupPicker(index) {
|
||
groupPickerOpen.value = groupPickerOpen.value === index ? null : index
|
||
}
|
||
|
||
/* ── Close on outside click for group picker ── */
|
||
|
||
function onBodyClick(e) {
|
||
if (groupPickerOpen.value !== null && !e.target.closest('[data-group-picker]')) {
|
||
groupPickerOpen.value = null
|
||
}
|
||
}
|
||
|
||
/* ── Keyboard: Escape ── */
|
||
|
||
function closeOnEscape(e) {
|
||
if (e.key === 'Escape') {
|
||
if (groupPickerOpen.value !== null) {
|
||
groupPickerOpen.value = null
|
||
} else {
|
||
emit('close')
|
||
}
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
document.addEventListener('keydown', closeOnEscape)
|
||
document.addEventListener('click', onBodyClick)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
document.removeEventListener('keydown', closeOnEscape)
|
||
document.removeEventListener('click', onBodyClick)
|
||
})
|
||
|
||
/* ── Arrangement CRUD (Inertia router, matching ArrangementConfigurator patterns) ── */
|
||
|
||
function createArrangement() {
|
||
const name = window.prompt('Name für das neue Arrangement:')
|
||
if (!name?.trim()) return
|
||
pendingAutoSelect.value = true
|
||
router.post(`/songs/${props.songId}/arrangements`, { name: name.trim() }, { preserveScroll: true })
|
||
}
|
||
|
||
function cloneArrangement() {
|
||
if (!currentArrangementId.value) return
|
||
const name = window.prompt('Name für das geklonte Arrangement:', `${currentArrangement.value?.name ?? ''} Kopie`)
|
||
if (!name?.trim()) return
|
||
pendingAutoSelect.value = true
|
||
router.post(`/arrangements/${currentArrangementId.value}/clone`, { name: name.trim() }, { preserveScroll: true })
|
||
}
|
||
|
||
function deleteArrangement() {
|
||
if (!currentArrangementId.value) return
|
||
if (props.arrangements.length <= 1) {
|
||
alert('Das letzte Arrangement kann nicht gelöscht werden.')
|
||
return
|
||
}
|
||
if (!confirm('Arrangement wirklich löschen?')) return
|
||
router.delete(`/arrangements/${currentArrangementId.value}`, { preserveScroll: true })
|
||
}
|
||
|
||
function saveArrangement() {
|
||
if (!currentArrangement.value) return
|
||
router.put(
|
||
`/arrangements/${currentArrangement.value.id}`,
|
||
{
|
||
groups: arrangementGroups.value.map((group, index) => ({
|
||
song_group_id: group.id,
|
||
order: index + 1,
|
||
})),
|
||
},
|
||
{
|
||
preserveScroll: true,
|
||
preserveState: true,
|
||
},
|
||
)
|
||
}
|
||
|
||
/* ── Group operations ── */
|
||
|
||
function duplicateGroup(index) {
|
||
const item = arrangementGroups.value[index]
|
||
const newItem = { ...item, _uid: `${item.id}-dup-${Date.now()}` }
|
||
arrangementGroups.value.splice(index + 1, 0, newItem)
|
||
saveArrangement()
|
||
}
|
||
|
||
function addGroupAt(index, group) {
|
||
const newItem = {
|
||
id: group.id,
|
||
name: group.name,
|
||
color: group.color,
|
||
slides: group.slides ?? [],
|
||
_uid: `${group.id}-add-${Date.now()}`,
|
||
}
|
||
arrangementGroups.value.splice(index + 1, 0, newItem)
|
||
groupPickerOpen.value = null
|
||
saveArrangement()
|
||
}
|
||
|
||
function removeGroupAt(index) {
|
||
arrangementGroups.value.splice(index, 1)
|
||
saveArrangement()
|
||
}
|
||
|
||
/* ── Selection emit ── */
|
||
|
||
watch(currentArrangementId, (id) => {
|
||
emit('arrangement-selected', Number(id))
|
||
})
|
||
|
||
/* ── Close on backdrop ── */
|
||
|
||
function closeOnBackdrop(e) {
|
||
if (e.target === e.currentTarget) {
|
||
emit('close')
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<Teleport to="body">
|
||
<Transition
|
||
enter-active-class="transition duration-200 ease-out"
|
||
enter-from-class="opacity-0"
|
||
enter-to-class="opacity-100"
|
||
leave-active-class="transition duration-150 ease-in"
|
||
leave-from-class="opacity-100"
|
||
leave-to-class="opacity-0"
|
||
>
|
||
<div
|
||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
||
@click="closeOnBackdrop"
|
||
>
|
||
<div
|
||
data-testid="arrangement-dialog"
|
||
:class="isUnmatched
|
||
? 'flex w-full max-w-lg flex-col rounded-xl bg-white shadow-2xl'
|
||
: 'flex h-[80vh] w-full max-w-5xl flex-col rounded-xl bg-white shadow-2xl'"
|
||
>
|
||
<!-- Header: Unmatched song assignment -->
|
||
<div v-if="isUnmatched" class="flex items-center gap-3 border-b border-gray-200 px-6 py-4">
|
||
<h3 class="text-base font-semibold text-gray-900">Song zuordnen</h3>
|
||
<button
|
||
data-testid="arrangement-dialog-close-btn"
|
||
type="button"
|
||
class="ml-auto rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
||
@click="emit('close')"
|
||
>
|
||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Header: Arrangement editor -->
|
||
<div v-else class="flex items-center gap-3 border-b border-gray-200 px-6 py-4">
|
||
<div class="min-w-48 flex-1">
|
||
<select
|
||
v-model="currentArrangementId"
|
||
data-testid="arrangement-select"
|
||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||
>
|
||
<option
|
||
v-for="arr in arrangements"
|
||
:key="arr.id"
|
||
:value="arr.id"
|
||
>
|
||
{{ arr.name }}{{ arr.is_default ? ' (Standard)' : '' }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
|
||
<button
|
||
data-testid="arrangement-new-btn"
|
||
type="button"
|
||
class="rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow hover:bg-blue-700"
|
||
@click="createArrangement"
|
||
>
|
||
Neu
|
||
</button>
|
||
|
||
<button
|
||
data-testid="arrangement-clone-btn"
|
||
type="button"
|
||
class="rounded-md bg-slate-700 px-3 py-2 text-sm font-semibold text-white shadow hover:bg-slate-800"
|
||
@click="cloneArrangement"
|
||
>
|
||
Duplizieren
|
||
</button>
|
||
|
||
<button
|
||
data-testid="arrangement-delete-btn"
|
||
type="button"
|
||
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow hover:bg-red-700"
|
||
@click="deleteArrangement"
|
||
>
|
||
Löschen
|
||
</button>
|
||
|
||
<button
|
||
data-testid="arrangement-dialog-close-btn"
|
||
type="button"
|
||
class="ml-auto rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
||
@click="emit('close')"
|
||
>
|
||
<svg
|
||
class="h-5 w-5"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
d="M6 18L18 6M6 6l12 12"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Body: Unmatched song search & assign -->
|
||
<div v-if="isUnmatched" class="flex-1 overflow-y-auto p-6">
|
||
<div class="mx-auto max-w-lg space-y-4">
|
||
<p class="text-sm text-gray-600">
|
||
Dieser CTS-Song ist noch nicht in der Song-DB verknüpft. Suche den passenden Song und ordne ihn zu.
|
||
</p>
|
||
|
||
<div class="relative">
|
||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||
Song suchen und auswählen
|
||
</label>
|
||
<input
|
||
v-model="searchQuery"
|
||
type="text"
|
||
placeholder="Titel oder CCLI eingeben…"
|
||
class="block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-emerald-500 focus:ring-emerald-500"
|
||
data-testid="song-search-input"
|
||
@focus="openSearchDropdown"
|
||
@input="openSearchDropdown"
|
||
@blur="closeSearchDropdown"
|
||
/>
|
||
<!-- Selected indicator -->
|
||
<div v-if="selectedSongId" class="mt-1 flex items-center gap-2">
|
||
<span class="text-xs font-medium text-emerald-700">
|
||
Ausgewählt: {{ songsCatalog.find((s) => s.id === Number(selectedSongId))?.title || '' }}
|
||
</span>
|
||
<button
|
||
type="button"
|
||
class="text-xs text-gray-400 hover:text-red-500"
|
||
@click="selectedSongId = ''; searchQuery = ''"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<!-- Dropdown -->
|
||
<div
|
||
v-if="dropdownOpen && filteredCatalog.length > 0"
|
||
class="absolute z-30 mt-1 max-h-64 w-full overflow-auto rounded-md border border-gray-200 bg-white shadow-lg"
|
||
>
|
||
<button
|
||
v-for="song in filteredCatalog"
|
||
:key="song.id"
|
||
type="button"
|
||
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-emerald-50"
|
||
@mousedown.prevent="selectSong(song)"
|
||
>
|
||
<span class="font-medium text-gray-900">{{ song.title }}</span>
|
||
<span class="ml-auto text-xs text-gray-400">CCLI: {{ song.ccli_id || '–' }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<p v-if="assignError" class="text-sm text-red-600">{{ assignError }}</p>
|
||
|
||
<button
|
||
type="button"
|
||
class="inline-flex items-center justify-center rounded-md bg-emerald-600 px-5 py-2 text-sm font-semibold text-white shadow transition hover:bg-emerald-700"
|
||
data-testid="song-assign-button"
|
||
@click="assignSong"
|
||
>
|
||
Zuordnen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Body: Two-column arrangement editor -->
|
||
<div v-else class="flex flex-1 overflow-hidden">
|
||
<!-- Left: Pill list with DnD -->
|
||
<div
|
||
ref="leftCol"
|
||
data-testid="arrangement-pill-list"
|
||
class="flex w-1/2 flex-col gap-2 overflow-y-auto border-r border-gray-200 p-4"
|
||
@scroll="syncScroll('left')"
|
||
>
|
||
<h4 class="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
||
Gruppenfolge
|
||
</h4>
|
||
|
||
<VueDraggable
|
||
v-model="arrangementGroups"
|
||
ghost-class="drag-ghost"
|
||
chosen-class="drag-chosen"
|
||
drag-class="drag-active"
|
||
handle=".drag-handle"
|
||
class="flex flex-col gap-2"
|
||
@end="saveArrangement"
|
||
>
|
||
<div
|
||
v-for="(element, index) in arrangementGroups"
|
||
:key="element._uid"
|
||
data-testid="arrangement-pill"
|
||
class="flex items-center gap-2 rounded-lg border-2 px-3 py-2"
|
||
:style="{
|
||
borderColor: element.color ?? '#6b7280',
|
||
backgroundColor: (element.color ?? '#6b7280') + '20',
|
||
}"
|
||
>
|
||
<!-- Drag handle -->
|
||
<span class="drag-handle cursor-grab text-gray-400 hover:text-gray-600">
|
||
<svg
|
||
class="h-4 w-4"
|
||
fill="currentColor"
|
||
viewBox="0 0 20 20"
|
||
>
|
||
<path d="M7 2a2 2 0 10.001 4.001A2 2 0 007 2zm0 6a2 2 0 10.001 4.001A2 2 0 007 8zm0 6a2 2 0 10.001 4.001A2 2 0 007 14zm6-8a2 2 0 10-.001-4.001A2 2 0 0013 6zm0 2a2 2 0 10.001 4.001A2 2 0 0013 8zm0 6a2 2 0 10.001 4.001A2 2 0 0013 14z" />
|
||
</svg>
|
||
</span>
|
||
|
||
<!-- Group name -->
|
||
<span class="flex-1 text-sm font-medium">
|
||
{{ element.name }}
|
||
</span>
|
||
|
||
<!-- Duplicate button (2×) -->
|
||
<button
|
||
data-testid="arrangement-duplicate-btn"
|
||
type="button"
|
||
class="rounded bg-gray-100 px-2 py-0.5 text-xs hover:bg-gray-200"
|
||
@click="duplicateGroup(index)"
|
||
>
|
||
2×
|
||
</button>
|
||
|
||
<!-- Add group button (+) -->
|
||
<div
|
||
class="relative"
|
||
data-group-picker
|
||
>
|
||
<button
|
||
data-testid="arrangement-add-group-btn"
|
||
type="button"
|
||
class="rounded bg-blue-100 px-2 py-0.5 text-xs text-blue-700 hover:bg-blue-200"
|
||
@click.stop="toggleGroupPicker(index)"
|
||
>
|
||
+
|
||
</button>
|
||
|
||
<!-- Quick select dropdown -->
|
||
<div
|
||
v-if="groupPickerOpen === index"
|
||
class="absolute right-0 top-8 z-10 min-w-36 rounded-lg border border-gray-200 bg-white shadow-lg"
|
||
>
|
||
<button
|
||
v-for="g in availableGroups"
|
||
:key="g.id"
|
||
type="button"
|
||
class="block w-full px-3 py-1.5 text-left text-sm hover:bg-gray-50"
|
||
@click="addGroupAt(index, g)"
|
||
>
|
||
<span
|
||
class="mr-1.5 inline-block h-2.5 w-2.5 rounded-full"
|
||
:style="{ backgroundColor: g.color }"
|
||
/>
|
||
{{ g.name }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Remove button -->
|
||
<button
|
||
data-testid="arrangement-remove-btn"
|
||
type="button"
|
||
class="rounded px-1.5 py-0.5 text-xs text-red-400 hover:bg-red-50 hover:text-red-600"
|
||
@click="removeGroupAt(index)"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</VueDraggable>
|
||
|
||
<!-- Empty state -->
|
||
<div
|
||
v-if="arrangementGroups.length === 0"
|
||
class="flex flex-1 items-center justify-center rounded-lg border-2 border-dashed border-gray-300 p-8 text-center text-sm text-gray-400"
|
||
>
|
||
Noch keine Gruppen zugeordnet.
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right: Lyric preview (scroll-synced) -->
|
||
<div
|
||
ref="rightCol"
|
||
data-testid="arrangement-lyric-preview"
|
||
class="flex w-1/2 flex-col gap-2 overflow-y-auto bg-gray-50 p-4"
|
||
@scroll="syncScroll('right')"
|
||
>
|
||
<h4 class="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
||
Textvorschau
|
||
</h4>
|
||
|
||
<div
|
||
v-for="element in arrangementGroups"
|
||
:key="element._uid"
|
||
class="rounded-r-lg border-l-4 bg-white p-3 shadow-sm"
|
||
:style="{ borderColor: element.color ?? '#6b7280' }"
|
||
>
|
||
<!-- Group name pill -->
|
||
<span
|
||
class="mb-2 inline-block rounded-full px-2 py-0.5 text-xs font-semibold text-white"
|
||
:style="{ backgroundColor: element.color ?? '#6b7280' }"
|
||
>
|
||
{{ element.name }}
|
||
</span>
|
||
|
||
<!-- Slides -->
|
||
<template v-if="element.slides?.length > 0">
|
||
<div
|
||
v-for="(slide, si) in element.slides"
|
||
:key="slide.id ?? si"
|
||
>
|
||
<div
|
||
v-if="si > 0"
|
||
class="my-1.5 border-t border-gray-200"
|
||
/>
|
||
<p class="whitespace-pre-wrap text-sm">
|
||
{{ slide.text_content }}
|
||
</p>
|
||
<p
|
||
v-if="slide.text_content_translated"
|
||
class="mt-0.5 whitespace-pre-wrap text-xs italic text-gray-400"
|
||
>
|
||
{{ slide.text_content_translated }}
|
||
</p>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- No slides -->
|
||
<p
|
||
v-else
|
||
class="text-xs italic text-gray-300"
|
||
>
|
||
Kein Text vorhanden
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Empty state -->
|
||
<div
|
||
v-if="arrangementGroups.length === 0"
|
||
class="flex flex-1 items-center justify-center text-sm text-gray-400"
|
||
>
|
||
Wähle ein Arrangement aus, um die Vorschau zu sehen.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</Teleport>
|
||
</template>
|
||
|
||
<style scoped>
|
||
:deep(.drag-ghost) {
|
||
opacity: 0.4;
|
||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8), 0 0 0 4px rgba(99, 102, 241, 0.6);
|
||
border-radius: 0.5rem;
|
||
}
|
||
|
||
:deep(.drag-chosen) {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
:deep(.drag-active) {
|
||
opacity: 1 !important;
|
||
transform: scale(1.02);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 2px rgba(255, 255, 255, 0.8);
|
||
border-radius: 0.5rem;
|
||
z-index: 50;
|
||
}
|
||
</style>
|