Files
Virtnet.bond/themes/lucentui/views/components/notification.blade.php
Joseph.Rawlings 8baf7d6c7d feat: Enhance UI components and add command palette
- 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.
2026-04-22 10:40:33 -04:00

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>