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,98 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, provide } from 'vue'
|
||||
import ToastNotification from './ToastNotification.vue'
|
||||
|
||||
interface Toast {
|
||||
id: string
|
||||
type: 'success' | 'error' | 'warning' | 'info'
|
||||
message: string
|
||||
duration?: number
|
||||
}
|
||||
|
||||
const toasts = ref<Toast[]>([])
|
||||
let toastId = 0
|
||||
|
||||
const addToast = (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info', duration = 4000) => {
|
||||
const id = `toast-${toastId++}`
|
||||
toasts.value.push({ id, type, message, duration })
|
||||
return id
|
||||
}
|
||||
|
||||
const removeToast = (id: string) => {
|
||||
toasts.value = toasts.value.filter(t => t.id !== id)
|
||||
}
|
||||
|
||||
// Provide toast API for child components
|
||||
provide('toast', {
|
||||
success: (msg: string, duration?: number) => addToast(msg, 'success', duration),
|
||||
error: (msg: string, duration?: number) => addToast(msg, 'error', duration),
|
||||
warning: (msg: string, duration?: number) => addToast(msg, 'warning', duration),
|
||||
info: (msg: string, duration?: number) => addToast(msg, 'info', duration),
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
addToast,
|
||||
removeToast,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toast-container">
|
||||
<transition-group name="toast-group" tag="div" class="toast-list">
|
||||
<ToastNotification
|
||||
v-for="toast in toasts"
|
||||
:key="toast.id"
|
||||
:id="toast.id"
|
||||
:type="toast.type"
|
||||
:message="toast.message"
|
||||
:duration="toast.duration"
|
||||
@close="removeToast"
|
||||
/>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: var(--spacing-4);
|
||||
right: var(--spacing-4);
|
||||
z-index: var(--z-index-tooltip);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.toast-group-enter-active,
|
||||
.toast-group-leave-active {
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.toast-group-enter-from,
|
||||
.toast-group-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
.toast-group-move {
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.toast-container {
|
||||
left: var(--spacing-2);
|
||||
right: var(--spacing-2);
|
||||
top: auto;
|
||||
bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.toast-list {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user