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>
99 lines
2.2 KiB
Vue
99 lines
2.2 KiB
Vue
<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>
|