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:
2026-08-15 11:28:20 +09:00
parent 79bfac8a28
commit d3760cccb5
11 changed files with 2686 additions and 425 deletions
@@ -0,0 +1,176 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
interface Props {
id: string
type?: 'success' | 'error' | 'warning' | 'info'
message: string
duration?: number
}
const props = withDefaults(defineProps<Props>(), {
type: 'info',
duration: 4000,
})
const emit = defineEmits<{
close: [id: string]
}>()
const isClosing = ref(false)
onMounted(() => {
if (props.duration > 0) {
setTimeout(() => {
close()
}, props.duration)
}
})
const close = () => {
isClosing.value = true
setTimeout(() => {
emit('close', props.id)
}, 150) // Match animation duration
}
const getIcon = (type: string) => {
const icons: Record<string, string> = {
success: '✓',
error: '✕',
warning: '!',
info: '',
}
return icons[type] || ''
}
const getColor = (type: string) => {
const colors: Record<string, string> = {
success: 'var(--color-success-500)',
error: 'var(--color-danger-500)',
warning: 'var(--color-warning-500)',
info: 'var(--color-primary-500)',
}
return colors[type] || 'var(--color-primary-500)'
}
</script>
<template>
<div class="toast-notification" :class="[`toast-${type}`, { 'is-closing': isClosing }]">
<div class="toast-icon" :style="{ backgroundColor: getColor(type) }">
{{ getIcon(type) }}
</div>
<div class="toast-content">
<p class="toast-message">{{ message }}</p>
</div>
<button class="toast-close" @click="close" aria-label="Close notification">
×
</button>
</div>
</template>
<style scoped>
.toast-notification {
display: flex;
align-items: center;
gap: var(--spacing-3);
padding: var(--spacing-3);
background: var(--color-background-primary);
border: var(--border-width-1) solid var(--color-border-primary);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-lg);
animation: slideIn 150ms ease-out;
min-width: 300px;
max-width: 500px;
}
.toast-notification.is-closing {
animation: slideOut 150ms ease-out forwards;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
.toast-icon {
display: flex;
align-items: center;
justify-content: center;
min-width: 32px;
width: 32px;
height: 32px;
border-radius: var(--border-radius-base);
color: white;
font-weight: var(--font-weight-bold);
font-size: var(--font-size-lg);
flex-shrink: 0;
}
.toast-content {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--spacing-1);
}
.toast-message {
margin: 0;
font-size: var(--font-size-sm);
color: var(--color-text-primary);
line-height: var(--line-height-normal);
}
.toast-close {
padding: 0;
width: 24px;
height: 24px;
min-width: 24px;
border: none;
background: transparent;
color: var(--color-text-tertiary);
cursor: pointer;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-light);
transition: color var(--transition-fast);
flex-shrink: 0;
}
.toast-close:hover {
color: var(--color-text-secondary);
}
/* Type-specific colors */
.toast-success .toast-icon {
background: var(--color-success-500);
}
.toast-error .toast-icon {
background: var(--color-danger-500);
}
.toast-warning .toast-icon {
background: var(--color-warning-500);
}
.toast-info .toast-icon {
background: var(--color-primary-500);
}
</style>