Files
gremiumhub/legalconsenthub/app/components/FormValidationIndicator.vue

214 lines
4.4 KiB
Vue

<template>
<div class="flex items-center gap-2">
<div class="relative flex items-center justify-center">
<svg
class="shield-ring"
:class="ringClass"
width="48"
height="48"
viewBox="0 0 48 48"
>
<circle
class="ring-background"
cx="24"
cy="24"
r="20"
fill="none"
stroke="currentColor"
stroke-width="3"
opacity="0.1"
/>
<circle
class="ring-progress"
cx="24"
cy="24"
r="20"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
:stroke-dasharray="circumference"
:stroke-dashoffset="progressOffset"
transform="rotate(-90 24 24)"
/>
</svg>
<Transition name="shield-pulse" mode="out-in">
<UIcon
:key="status"
:name="shieldIcon"
class="absolute shield-icon"
:class="iconClass"
/>
</Transition>
</div>
<UBadge
:label="statusLabel"
:color="badgeColor"
size="md"
variant="subtle"
class="status-badge"
/>
</div>
</template>
<script setup lang="ts">
import { ComplianceStatus } from '~~/.api-client'
const props = defineProps<{
status: ComplianceStatus
}>()
const circumference = 2 * Math.PI * 20
const shieldIcon = computed(() => {
switch (props.status) {
case ComplianceStatus.Critical:
return 'i-lucide-shield-alert'
case ComplianceStatus.Warning:
return 'i-lucide-shield-x'
case ComplianceStatus.NonCritical:
return 'i-lucide-shield-check'
default:
return 'i-lucide-shield-check'
}
})
const statusLabel = computed(() => {
switch (props.status) {
case ComplianceStatus.Critical:
return 'Kritisch'
case ComplianceStatus.Warning:
return 'Warnung'
case ComplianceStatus.NonCritical:
return 'Unkritisch'
default:
return 'Unkritisch'
}
})
const badgeColor = computed(() => {
switch (props.status) {
case ComplianceStatus.Critical:
return 'red'
case ComplianceStatus.Warning:
return 'yellow'
case ComplianceStatus.NonCritical:
return 'green'
default:
return 'green'
}
})
const ringClass = computed(() => {
switch (props.status) {
case ComplianceStatus.Critical:
return 'text-red-500'
case ComplianceStatus.Warning:
return 'text-yellow-500'
case ComplianceStatus.NonCritical:
return 'text-green-500'
default:
return 'text-green-500'
}
})
const iconClass = computed(() => {
switch (props.status) {
case ComplianceStatus.Critical:
return 'text-red-500 w-6 h-6'
case ComplianceStatus.Warning:
return 'text-yellow-500 w-6 h-6'
case ComplianceStatus.NonCritical:
return 'text-green-500 w-6 h-6'
default:
return 'text-green-500 w-6 h-6'
}
})
const progressValue = computed(() => {
switch (props.status) {
case ComplianceStatus.Critical:
return 100
case ComplianceStatus.Warning:
return 60
case ComplianceStatus.NonCritical:
return 30
default:
return 30
}
})
const progressOffset = computed(() => {
return circumference - (progressValue.value / 100) * circumference
})
watch(() => props.status, () => {
const element = document.querySelector('.shield-ring')
if (element) {
element.classList.add('status-change-pulse')
setTimeout(() => {
element.classList.remove('status-change-pulse')
}, 600)
}
})
</script>
<style scoped>
.shield-ring {
transition: color 300ms ease-in-out;
}
.ring-progress {
transition: stroke-dashoffset 500ms cubic-bezier(0.4, 0, 0.2, 1);
}
.shield-icon {
transition: color 300ms ease-in-out;
}
.status-badge {
transition: all 300ms ease-in-out;
}
@keyframes pulse-ring {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.status-change-pulse {
animation: pulse-ring 600ms ease-in-out;
}
.shield-pulse-enter-active,
.shield-pulse-leave-active {
transition: all 300ms ease-in-out;
}
.shield-pulse-enter-from {
opacity: 0;
transform: scale(0.8);
}
.shield-pulse-leave-to {
opacity: 0;
transform: scale(1.2);
}
.shield-icon {
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
}
</style>