- Updated dropdown component styles and transitions for improved UX. - Refined checkbox, input, radio, select, and textarea components with consistent error messaging and styling. - Introduced a new card component for better layout options. - Added a stat card component for displaying metrics with trends. - Implemented a command palette for global search and navigation, accessible via Cmd/Ctrl+K. - Enhanced modal and navigation components with new styles and transitions. - Improved layout styles for better responsiveness and visual appeal.
131 lines
8.6 KiB
PHP
131 lines
8.6 KiB
PHP
{{--
|
|
Toast v2 — supports: success | error | info | warning types,
|
|
positions (top|bottom)-(left|right|center), action buttons,
|
|
swipe-to-dismiss, pause-on-hover. Backward compatible with
|
|
existing `$this->dispatch('notify', [...])` calls.
|
|
--}}
|
|
<div x-data>
|
|
@foreach (['top-right','bottom-right','top-left','bottom-left','top-center','bottom-center'] as $pos)
|
|
@php
|
|
[$vy, $hx] = explode('-', $pos);
|
|
$posClasses = [
|
|
'top-right' => 'top-4 right-4 items-end',
|
|
'bottom-right' => 'bottom-4 right-4 items-end flex-col-reverse',
|
|
'top-left' => 'top-4 left-4 items-start',
|
|
'bottom-left' => 'bottom-4 left-4 items-start flex-col-reverse',
|
|
'top-center' => 'top-4 left-1/2 -translate-x-1/2 items-center',
|
|
'bottom-center'=> 'bottom-4 left-1/2 -translate-x-1/2 items-center flex-col-reverse',
|
|
][$pos];
|
|
$enterFromClass = match($hx) {
|
|
'left' => 'opacity-0 -translate-x-full scale-95',
|
|
'center' => 'opacity-0 scale-95 ' . ($vy === 'top' ? '-translate-y-4' : 'translate-y-4'),
|
|
default => 'opacity-0 translate-x-full scale-95',
|
|
};
|
|
$leaveToClass = $enterFromClass;
|
|
@endphp
|
|
<div class="fixed z-50 flex flex-col gap-3 max-w-sm w-full pointer-events-none {{ $posClasses }}">
|
|
<template x-for="notification in $store.notifications.notifications.filter(n => (n.position || 'bottom-right') === '{{ $pos }}')" :key="notification.id">
|
|
<div x-show="notification.show"
|
|
x-data="{
|
|
dragX: 0,
|
|
dragging: false,
|
|
startX: 0,
|
|
reduced: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
|
onDown(e) {
|
|
if (this.reduced) return;
|
|
this.dragging = true;
|
|
this.startX = e.clientX;
|
|
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
},
|
|
onMove(e) {
|
|
if (!this.dragging) return;
|
|
this.dragX = e.clientX - this.startX;
|
|
},
|
|
onUp(e) {
|
|
if (!this.dragging) return;
|
|
this.dragging = false;
|
|
if (Math.abs(this.dragX) > 80) {
|
|
$store.notifications.removeNotification(notification.id);
|
|
} else {
|
|
this.dragX = 0;
|
|
}
|
|
}
|
|
}"
|
|
@mouseenter="$store.notifications.pauseNotification(notification.id)"
|
|
@mouseleave="$store.notifications.resumeNotification(notification.id)"
|
|
@pointerdown="onDown($event)"
|
|
@pointermove="onMove($event)"
|
|
@pointerup="onUp($event)"
|
|
@pointercancel="onUp($event)"
|
|
x-transition:enter="transition ease-out duration-300 transform"
|
|
x-transition:enter-start="{{ $enterFromClass }}"
|
|
x-transition:enter-end="opacity-100 translate-x-0 translate-y-0 scale-100"
|
|
x-transition:leave="transition ease-in duration-200 transform"
|
|
x-transition:leave-start="opacity-100 translate-x-0 translate-y-0 scale-100"
|
|
x-transition:leave-end="{{ $leaveToClass }}"
|
|
:style="`transform: translateX(${dragX}px); opacity: ${1 - Math.min(Math.abs(dragX)/160, 0.5)}; transition: ${dragging ? 'none' : 'transform 240ms var(--ease-out-soft), opacity 240ms var(--ease-out-soft)'}; touch-action: pan-y;`"
|
|
:class="{
|
|
'bg-success border-success/50': notification.type === 'success',
|
|
'bg-error border-error/50': notification.type === 'error',
|
|
'bg-info border-info/50': notification.type === 'info',
|
|
'bg-warning border-warning/60': notification.type === 'warning',
|
|
}"
|
|
class="relative flex flex-col text-white rounded-xl shadow-floating border backdrop-blur-sm pointer-events-auto overflow-hidden select-none cursor-grab active:cursor-grabbing">
|
|
<div class="flex items-start justify-between gap-3 px-4 py-3">
|
|
<div class="flex items-start gap-3 min-w-0 flex-1">
|
|
<template x-if="notification.type === 'success'">
|
|
<svg class="size-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/></svg>
|
|
</template>
|
|
<template x-if="notification.type === 'error'">
|
|
<svg class="size-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"/></svg>
|
|
</template>
|
|
<template x-if="notification.type === 'info'">
|
|
<svg class="size-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
</template>
|
|
<template x-if="notification.type === 'warning'">
|
|
<svg class="size-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 9v2m0 4h.01M4.93 19h14.14a2 2 0 001.74-3l-7.07-12a2 2 0 00-3.48 0L3.19 16a2 2 0 001.74 3z"/></svg>
|
|
</template>
|
|
<div class="min-w-0 flex-1">
|
|
<p x-text="notification.title || ''" x-show="notification.title" class="text-sm font-bold mb-0.5"></p>
|
|
<p x-text="notification.message" class="text-sm font-medium leading-snug break-words"></p>
|
|
{{-- Action buttons --}}
|
|
<template x-if="notification.actions && notification.actions.length > 0">
|
|
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
<template x-for="(action, aIdx) in notification.actions" :key="aIdx">
|
|
<button type="button"
|
|
@click.stop="$store.notifications.invokeAction(notification.id, aIdx)"
|
|
class="inline-flex items-center gap-1 text-xs font-semibold px-2.5 py-1 rounded-md bg-white/15 hover:bg-white/25 active:scale-95 transition-all duration-150"
|
|
x-text="action.label"></button>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<button @click.stop="$store.notifications.removeNotification(notification.id)"
|
|
aria-label="Dismiss"
|
|
class="text-white/70 hover:text-white transition-colors p-1 rounded-full hover:bg-white/10 shrink-0">
|
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
|
</button>
|
|
</div>
|
|
{{-- Auto-dismiss progress bar (pauses on hover) --}}
|
|
<div class="h-1 bg-black/20">
|
|
<div class="h-full bg-white/70 notification-progress"
|
|
:style="`animation-duration: ${notification.timeout || 5000}ms; animation-play-state: ${notification.paused ? 'paused' : 'running'};`"></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
<style>
|
|
@keyframes notification-countdown {
|
|
from { width: 100%; }
|
|
to { width: 0%; }
|
|
}
|
|
.notification-progress {
|
|
animation: notification-countdown 5000ms linear forwards;
|
|
transform-origin: left center;
|
|
}
|
|
</style>
|