d3760cccb5
**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>
146 lines
3.5 KiB
Vue
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>
|