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

201 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 { t: $t } = useI18n()
const statusLabel = computed(() => {
switch (props.status) {
case ComplianceStatus.Critical:
return $t('compliance.critical')
case ComplianceStatus.Warning:
return $t('compliance.warning')
case ComplianceStatus.NonCritical:
return $t('compliance.nonCritical')
default:
return $t('compliance.nonCritical')
}
})
const badgeColor = computed(() => {
switch (props.status) {
case ComplianceStatus.Critical:
return 'error' as const
case ComplianceStatus.Warning:
return 'warning' as const
case ComplianceStatus.NonCritical:
return 'success' as const
default:
return 'success' as const
}
})
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>