generated from corrad-software/corrad-af-2024
269 lines
7.6 KiB
Vue
269 lines
7.6 KiB
Vue
<script setup>
|
|
import { computed } from 'vue';
|
|
import { useDesignSystem } from '~/composables/useDesignSystem';
|
|
|
|
const props = defineProps({
|
|
// Loading type
|
|
type: {
|
|
type: String,
|
|
default: 'spinner',
|
|
validator: (value) => [
|
|
'spinner', 'skeleton', 'skeleton-table', 'skeleton-card',
|
|
'skeleton-list', 'skeleton-tree', 'pulse', 'overlay'
|
|
].includes(value)
|
|
},
|
|
|
|
// Size variants
|
|
size: {
|
|
type: String,
|
|
default: 'md',
|
|
validator: (value) => ['sm', 'md', 'lg', 'xl'].includes(value)
|
|
},
|
|
|
|
// Loading message
|
|
message: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
|
|
// Number of skeleton items
|
|
count: {
|
|
type: Number,
|
|
default: 3
|
|
},
|
|
|
|
// Show as overlay
|
|
overlay: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
// Animation speed
|
|
speed: {
|
|
type: String,
|
|
default: 'normal',
|
|
validator: (value) => ['slow', 'normal', 'fast'].includes(value)
|
|
},
|
|
|
|
// Custom classes
|
|
class: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
});
|
|
|
|
// Design system
|
|
const { tokens, utils } = useDesignSystem();
|
|
|
|
// Computed classes
|
|
const containerClasses = computed(() => {
|
|
const classes = ['loading-container'];
|
|
|
|
if (props.overlay) {
|
|
classes.push('loading-overlay');
|
|
}
|
|
|
|
if (props.class) {
|
|
classes.push(props.class);
|
|
}
|
|
|
|
return classes.join(' ');
|
|
});
|
|
|
|
const spinnerClasses = computed(() => {
|
|
const sizeMap = {
|
|
sm: 'w-4 h-4',
|
|
md: 'w-6 h-6',
|
|
lg: 'w-8 h-8',
|
|
xl: 'w-12 h-12'
|
|
};
|
|
|
|
const speedMap = {
|
|
slow: 'animate-spin-slow',
|
|
normal: 'animate-spin',
|
|
fast: 'animate-spin-fast'
|
|
};
|
|
|
|
return utils.composeClasses(
|
|
'border-2 border-current border-t-transparent rounded-full',
|
|
sizeMap[props.size],
|
|
speedMap[props.speed]
|
|
);
|
|
});
|
|
|
|
const pulseClasses = computed(() => {
|
|
const speedMap = {
|
|
slow: 'animate-pulse-slow',
|
|
normal: 'animate-pulse',
|
|
fast: 'animate-pulse-fast'
|
|
};
|
|
|
|
return utils.composeClasses(
|
|
'bg-gray-300 dark:bg-gray-600 rounded',
|
|
speedMap[props.speed]
|
|
);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div :class="containerClasses">
|
|
<!-- Spinner Loading -->
|
|
<div v-if="type === 'spinner'" class="flex flex-col items-center justify-center space-y-3">
|
|
<div :class="spinnerClasses" role="status" aria-label="Loading"></div>
|
|
<p v-if="message" class="text-sm text-gray-600 dark:text-gray-400">
|
|
{{ message }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Pulse Loading -->
|
|
<div v-else-if="type === 'pulse'" class="flex items-center justify-center">
|
|
<div class="flex space-x-2">
|
|
<div class="w-2 h-2 bg-primary rounded-full animate-bounce"></div>
|
|
<div class="w-2 h-2 bg-primary rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
|
|
<div class="w-2 h-2 bg-primary rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Skeleton Table -->
|
|
<div v-else-if="type === 'skeleton-table'" class="space-y-4">
|
|
<!-- Table Header -->
|
|
<div class="grid grid-cols-4 gap-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
<div v-for="i in 4" :key="`header-${i}`" :class="pulseClasses" class="h-4"></div>
|
|
</div>
|
|
|
|
<!-- Table Rows -->
|
|
<div v-for="row in count" :key="`row-${row}`" class="grid grid-cols-4 gap-4 p-4 border border-gray-200 dark:border-gray-600 rounded-lg">
|
|
<div v-for="col in 4" :key="`cell-${row}-${col}`" :class="pulseClasses" class="h-4"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Skeleton Cards -->
|
|
<div v-else-if="type === 'skeleton-card'" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
<div v-for="card in count" :key="`card-${card}`" class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg p-6 space-y-4">
|
|
<!-- Card Header -->
|
|
<div class="flex items-center space-x-3">
|
|
<div :class="pulseClasses" class="w-10 h-10 rounded-full"></div>
|
|
<div class="flex-1 space-y-2">
|
|
<div :class="pulseClasses" class="h-4 w-3/4"></div>
|
|
<div :class="pulseClasses" class="h-3 w-1/2"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card Content -->
|
|
<div class="space-y-2">
|
|
<div :class="pulseClasses" class="h-3 w-full"></div>
|
|
<div :class="pulseClasses" class="h-3 w-5/6"></div>
|
|
<div :class="pulseClasses" class="h-3 w-4/6"></div>
|
|
</div>
|
|
|
|
<!-- Card Footer -->
|
|
<div class="flex justify-between">
|
|
<div :class="pulseClasses" class="h-8 w-20 rounded"></div>
|
|
<div :class="pulseClasses" class="h-8 w-16 rounded"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Skeleton List -->
|
|
<div v-else-if="type === 'skeleton-list'" class="space-y-3">
|
|
<div v-for="item in count" :key="`list-${item}`" class="flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-600 rounded-lg">
|
|
<div :class="pulseClasses" class="w-8 h-8 rounded"></div>
|
|
<div class="flex-1 space-y-2">
|
|
<div :class="pulseClasses" class="h-4 w-3/4"></div>
|
|
<div :class="pulseClasses" class="h-3 w-1/2"></div>
|
|
</div>
|
|
<div :class="pulseClasses" class="w-6 h-6 rounded"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Skeleton Tree -->
|
|
<div v-else-if="type === 'skeleton-tree'" class="space-y-2">
|
|
<div v-for="level in count" :key="`tree-${level}`">
|
|
<!-- Parent Node -->
|
|
<div class="flex items-center space-x-2 p-2">
|
|
<div :class="pulseClasses" class="w-4 h-4 rounded"></div>
|
|
<div :class="pulseClasses" class="w-5 h-5 rounded"></div>
|
|
<div :class="pulseClasses" class="h-4 w-40"></div>
|
|
</div>
|
|
|
|
<!-- Child Nodes -->
|
|
<div class="ml-6 space-y-1">
|
|
<div v-for="child in 2" :key="`child-${level}-${child}`" class="flex items-center space-x-2 p-1">
|
|
<div :class="pulseClasses" class="w-4 h-4 rounded"></div>
|
|
<div :class="pulseClasses" class="h-3 w-32"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Basic Skeleton -->
|
|
<div v-else-if="type === 'skeleton'" class="space-y-3">
|
|
<div v-for="item in count" :key="`skeleton-${item}`" class="space-y-2">
|
|
<div :class="pulseClasses" class="h-4 w-full"></div>
|
|
<div :class="pulseClasses" class="h-4 w-5/6"></div>
|
|
<div :class="pulseClasses" class="h-4 w-4/6"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Overlay Loading -->
|
|
<div v-else-if="type === 'overlay'" class="overlay-content">
|
|
<div class="flex flex-col items-center justify-center space-y-4">
|
|
<div :class="spinnerClasses" role="status" aria-label="Loading"></div>
|
|
<p v-if="message" class="text-lg font-medium text-gray-700 dark:text-gray-300">
|
|
{{ message }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.loading-container {
|
|
@apply w-full;
|
|
}
|
|
|
|
.loading-overlay {
|
|
@apply fixed inset-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm;
|
|
}
|
|
|
|
.overlay-content {
|
|
@apply h-full flex items-center justify-center;
|
|
}
|
|
|
|
/* Custom animation speeds */
|
|
@keyframes spin-slow {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
@keyframes spin-fast {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
@keyframes pulse-slow {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.4; }
|
|
}
|
|
|
|
@keyframes pulse-fast {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.6; }
|
|
}
|
|
|
|
.animate-spin-slow {
|
|
animation: spin-slow 2s linear infinite;
|
|
}
|
|
|
|
.animate-spin-fast {
|
|
animation: spin-fast 0.5s linear infinite;
|
|
}
|
|
|
|
.animate-pulse-slow {
|
|
animation: pulse-slow 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
}
|
|
|
|
.animate-pulse-fast {
|
|
animation: pulse-fast 1s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
}
|
|
</style> |