feat(ui): compact toast notifications and skeleton editor selection
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="toast-container fixed top-4 right-4 z-50 space-y-2 pointer-events-none">
|
<div
|
||||||
|
class="toast-container pointer-events-none fixed bottom-4 right-4 z-50 flex flex-col gap-2 items-end"
|
||||||
|
>
|
||||||
<TransitionGroup name="toast">
|
<TransitionGroup name="toast">
|
||||||
<div
|
<div
|
||||||
v-for="toast in toasts"
|
v-for="toast in toasts"
|
||||||
@@ -7,11 +9,11 @@
|
|||||||
class="toast-item"
|
class="toast-item"
|
||||||
:class="[
|
:class="[
|
||||||
'transform transition-all duration-300 ease-in-out',
|
'transform transition-all duration-300 ease-in-out',
|
||||||
toast.visible ? 'translate-x-0 opacity-100 pointer-events-auto' : 'translate-x-full opacity-0 pointer-events-none'
|
toast.visible ? 'translate-y-0 opacity-100 pointer-events-auto' : 'translate-y-4 opacity-0 pointer-events-none'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="alert shadow-lg max-w-sm"
|
class="alert toast-card shadow-md px-3 py-2 text-sm"
|
||||||
:class="getToastClasses(toast.type)"
|
:class="getToastClasses(toast.type)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -19,37 +21,37 @@
|
|||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<IconLucideCheck
|
<IconLucideCheck
|
||||||
v-if="toast.type === 'success'"
|
v-if="toast.type === 'success'"
|
||||||
class="w-5 h-5"
|
class="w-4 h-4"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<IconLucideX
|
<IconLucideX
|
||||||
v-else-if="toast.type === 'error'"
|
v-else-if="toast.type === 'error'"
|
||||||
class="w-5 h-5"
|
class="w-4 h-4"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<IconLucideAlertTriangle
|
<IconLucideAlertTriangle
|
||||||
v-else-if="toast.type === 'warning'"
|
v-else-if="toast.type === 'warning'"
|
||||||
class="w-5 h-5"
|
class="w-4 h-4"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<IconLucideInfo
|
<IconLucideInfo
|
||||||
v-else
|
v-else
|
||||||
class="w-5 h-5"
|
class="w-4 h-4"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Message -->
|
<!-- Message -->
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<span class="text-sm font-medium">{{ toast.message }}</span>
|
<span class="font-medium">{{ toast.message }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Close button -->
|
<!-- Close button -->
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost btn-xs"
|
class="btn btn-ghost btn-2xs"
|
||||||
@click="removeToast(toast.id)"
|
@click="removeToast(toast.id)"
|
||||||
>
|
>
|
||||||
<IconLucideX class="w-4 h-4" aria-hidden="true" />
|
<IconLucideX class="w-3 h-3" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,13 +72,13 @@ const { toasts, removeToast } = useToast()
|
|||||||
const getToastClasses = (type) => {
|
const getToastClasses = (type) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'success':
|
case 'success':
|
||||||
return 'alert-success'
|
return 'alert-success text-success-content'
|
||||||
case 'error':
|
case 'error':
|
||||||
return 'alert-error'
|
return 'alert-error text-error-content'
|
||||||
case 'warning':
|
case 'warning':
|
||||||
return 'alert-warning'
|
return 'alert-warning text-warning-content'
|
||||||
case 'info':
|
case 'info':
|
||||||
return 'alert-info'
|
return 'alert-info text-info-content'
|
||||||
default:
|
default:
|
||||||
return 'alert-info'
|
return 'alert-info'
|
||||||
}
|
}
|
||||||
@@ -91,15 +93,21 @@ const getToastClasses = (type) => {
|
|||||||
|
|
||||||
.toast-enter-from {
|
.toast-enter-from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(100%);
|
transform: translateY(16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-leave-to {
|
.toast-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(100%);
|
transform: translateY(16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-move {
|
.toast-move {
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toast-card {
|
||||||
|
max-width: 20rem;
|
||||||
|
pointer-events: auto;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const toasts = ref([])
|
const toasts = ref([])
|
||||||
|
const MAX_TOASTS = 3
|
||||||
let nextId = 1
|
let nextId = 1
|
||||||
|
|
||||||
export function useToast () {
|
export function useToast () {
|
||||||
const showToast = (message, type = 'info', duration = 5000) => {
|
const showToast = (message, type = 'info', duration = 3500) => {
|
||||||
const id = nextId++
|
const id = nextId++
|
||||||
const toast = {
|
const toast = {
|
||||||
id,
|
id,
|
||||||
@@ -13,6 +14,10 @@ export function useToast () {
|
|||||||
visible: true
|
visible: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toasts.value.length >= MAX_TOASTS) {
|
||||||
|
toasts.value.shift()
|
||||||
|
}
|
||||||
|
|
||||||
toasts.value.push(toast)
|
toasts.value.push(toast)
|
||||||
|
|
||||||
// Auto-remove after duration
|
// Auto-remove after duration
|
||||||
@@ -27,7 +32,7 @@ export function useToast () {
|
|||||||
return showToast(message, 'success', duration)
|
return showToast(message, 'success', duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showError = (message, duration = 7000) => {
|
const showError = (message, duration = 5000) => {
|
||||||
return showToast(message, 'error', duration)
|
return showToast(message, 'error', duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user