Files
KArtSell.Aegis/frontend/src/shared/ui/components/SkeletonLoader.vue
T
kjh2064 d3760cccb5 feat: Phase 2 — UI component enhancement with design tokens
**Design System (Phase 1 completion):**
- tokens.ts: 12-category token system (colors, typography, spacing, shadows, etc.)
- tokens.css: CSS variables with light/dark theme support
- App.vue: Integrated token system app-wide

**Shared Components (Phase 2):**
- SkeletonLoader.vue: 5 loader types (text, card, avatar, table, list)
- ErrorBoundary.vue: Error state recovery with retry
- ToastNotification.vue: Toast alerts with 4 types + auto-dismiss
- ToastContainer.vue: Toast provider with fixed positioning
- Modal.vue: Animated modal with 4 size variants

**Page Enhancements:**
- ShadowRunQueue.vue: Loading → Skeleton + Error → ErrorBoundary + Normal states
- ModelList.vue: Loading → Skeleton + Error → ErrorBoundary + Normal states
- ApprovalQueue.vue: Loading → Skeleton + Error → ErrorBoundary + Normal states

**Testing:**
- All 3 pages:  100% PASS (12/12 selectors)
- Playwright tests verify: loading, error, normal states
- All states render correctly with token-based styling

**Next: Phase 3 (2-4h)**
- Layout unification (sidebar, header, footer)
- Complete dark theme implementation
- Mobile responsiveness & navigation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-15 11:28:20 +09:00

146 lines
3.5 KiB
Vue

<script setup lang="ts">
interface Props {
type?: 'text' | 'card' | 'avatar' | 'table' | 'list'
rows?: number
width?: string
height?: string
}
defineProps<Props>()
</script>
<template>
<div class="skeleton-loader" :class="[`skeleton-${type}`]">
<!-- Text skeleton -->
<template v-if="type === 'text'">
<div class="skeleton-line" :style="{ width, height: height || '16px' }"></div>
<div class="skeleton-line" :style="{ width: '90%', height: height || '16px', marginTop: '8px' }"></div>
<div class="skeleton-line" :style="{ width: '75%', height: height || '16px', marginTop: '8px' }"></div>
</template>
<!-- Card skeleton -->
<template v-else-if="type === 'card'">
<div class="skeleton-card">
<div class="skeleton-header">
<div class="skeleton-line" style="width: 80%; height: 24px"></div>
<div class="skeleton-avatar"></div>
</div>
<div class="skeleton-body">
<div class="skeleton-line" style="width: 100%; height: 16px"></div>
<div class="skeleton-line" style="width: 90%; height: 16px; margin-top: 8px"></div>
</div>
</div>
</template>
<!-- Avatar skeleton -->
<template v-else-if="type === 'avatar'">
<div class="skeleton-avatar"></div>
</template>
<!-- Table skeleton -->
<template v-else-if="type === 'table'">
<div class="skeleton-table">
<div v-for="i in (rows || 5)" :key="i" class="skeleton-table-row">
<div class="skeleton-cell"></div>
<div class="skeleton-cell"></div>
<div class="skeleton-cell"></div>
</div>
</div>
</template>
<!-- List skeleton -->
<template v-else-if="type === 'list'">
<div v-for="i in (rows || 3)" :key="i" class="skeleton-list-item">
<div class="skeleton-avatar"></div>
<div class="skeleton-list-content">
<div class="skeleton-line" style="width: 60%; height: 16px"></div>
<div class="skeleton-line" style="width: 40%; height: 12px; margin-top: 8px"></div>
</div>
</div>
</template>
</div>
</template>
<style scoped>
.skeleton-loader {
animation: pulse var(--transition-slow);
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
}
.skeleton-line {
background: var(--color-background-secondary);
border-radius: var(--border-radius-base);
display: block;
margin-bottom: var(--spacing-2);
}
.skeleton-avatar {
width: 40px;
height: 40px;
border-radius: var(--border-radius-full);
background: var(--color-background-secondary);
flex-shrink: 0;
}
.skeleton-card {
padding: var(--spacing-4);
background: var(--color-background-primary);
border: var(--border-width-1) solid var(--color-border-secondary);
border-radius: var(--border-radius-lg);
}
.skeleton-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-3);
}
.skeleton-body {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
.skeleton-table {
width: 100%;
}
.skeleton-table-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-2);
margin-bottom: var(--spacing-3);
}
.skeleton-cell {
height: 20px;
background: var(--color-background-secondary);
border-radius: var(--border-radius-base);
}
.skeleton-list-item {
display: flex;
gap: var(--spacing-3);
margin-bottom: var(--spacing-3);
}
.skeleton-list-content {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
</style>