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>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user