Files
KArtSell.Aegis/frontend/src/shared/ui/components/ToastNotification.vue
T
kjh2064 d3760cccb5 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>
2026-08-15 11:28:20 +09:00

177 lines
3.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>