diff --git a/themes/lucentui/css/app.css b/themes/lucentui/css/app.css index cd5f42f23..b06a01d61 100644 --- a/themes/lucentui/css/app.css +++ b/themes/lucentui/css/app.css @@ -312,6 +312,155 @@ html { .neon-border::before { animation: none !important; } + .border-gradient-live::before { + animation: none !important; + } +} + +/* ============================================================================= + HOVER LIFT — universal micro-interaction for list rows, cards, tiles. + ============================================================================= */ +@utility hover-lift { + transition: transform 220ms var(--ease-out-soft, ease), box-shadow 220ms var(--ease-out-soft, ease); +} +.hover-lift:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-floating); +} + +/* ============================================================================= + BORDER-GRADIENT-LIVE — slower animated conic rail for active status badges. + ============================================================================= */ +@utility border-gradient-live { + position: relative; + isolation: isolate; +} +.border-gradient-live::before { + content: ''; + position: absolute; + inset: -2px; + border-radius: inherit; + padding: 2px; + background: conic-gradient(from var(--neon-angle), + hsl(var(--color-success) / 0.85), + hsl(var(--color-primary) / 0.6), + hsl(var(--color-secondary) / 0.85), + hsl(var(--color-success) / 0.85)); + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + animation: neon-spin 12s linear infinite; + pointer-events: none; + z-index: -1; +} + +/* ============================================================================= + PULSE RING — double-ripple for live status dots (e.g. "Available"). + ============================================================================= */ +@utility pulse-ring { + position: relative; +} +.pulse-ring::before, +.pulse-ring::after { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + background: currentColor; + opacity: 0; + animation: pulse-ring-expand 2s cubic-bezier(0, 0, 0.2, 1) infinite; + pointer-events: none; +} +.pulse-ring::after { + animation-delay: 1s; +} +@keyframes pulse-ring-expand { + 0% { transform: scale(1); opacity: 0.6; } + 80% { transform: scale(2.4); opacity: 0; } + 100% { transform: scale(2.4); opacity: 0; } +} + +/* ============================================================================= + RIPPLE — radial ripple on :active for solid buttons (primary/danger). + Uses ::after so it doesn't conflict with existing ::before usage. + ============================================================================= */ +.btn-ripple { + position: relative; + overflow: hidden; + isolation: isolate; +} +.btn-ripple::after { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle at center, rgba(255,255,255,0.35) 0%, rgba(255,255,255,0) 60%); + opacity: 0; + transform: scale(0.4); + pointer-events: none; + transition: transform 0.45s var(--ease-out-soft, ease), opacity 0.45s var(--ease-out-soft, ease); +} +.btn-ripple:active::after { + opacity: 1; + transform: scale(1); + transition-duration: 0s; +} + +/* ============================================================================= + SCROLL-REVEAL STAGGER — extends .reveal-on-scroll with a per-index delay. + Parent sets --reveal-idx (0,1,2,…); delay = idx * 60ms. + ============================================================================= */ +[data-reveal-stagger] > .reveal-on-scroll, +.reveal-on-scroll[data-reveal-idx] { + transition-delay: calc(var(--reveal-idx, 0) * 60ms); +} + +/* ============================================================================= + FOCUS-VISIBLE UNIFICATION — consistent primary ring on keyboard focus, + globally. Individual form controls already customize their own rings. + ============================================================================= */ +@layer base { + :where(a, button, [role="button"], summary, [tabindex]):focus-visible { + outline: 2px solid hsl(var(--color-primary) / 0.6); + outline-offset: 2px; + border-radius: inherit; + } +} + +/* ============================================================================= + TILT SPOTLIGHT — when a card has [data-tilt], the Alpine directive sets + --tilt-x/--tilt-y (0..1). This renders a soft radial spotlight following the + cursor. The directive also applies perspective rotations via inline style. + ============================================================================= */ +[data-tilt] { + transition: transform 120ms var(--ease-out-soft, ease); + transform-style: preserve-3d; +} +[data-tilt]::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + background: radial-gradient( + 280px circle at calc(var(--tilt-x, 0.5) * 100%) calc(var(--tilt-y, 0.5) * 100%), + hsl(var(--color-primary) / 0.14), + transparent 65% + ); + opacity: 0; + transition: opacity 220ms var(--ease-out-soft, ease); + pointer-events: none; + z-index: 0; +} +[data-tilt]:hover::before { + opacity: 1; +} +[data-tilt] > * { + position: relative; + z-index: 1; +} +@media (prefers-reduced-motion: reduce) { + [data-tilt] { transform: none !important; } + [data-tilt]::before { display: none !important; } } /* Line clamp utilities - removed excessive containment */ diff --git a/themes/lucentui/js/app.js b/themes/lucentui/js/app.js index 68031d90a..9d85ad868 100644 --- a/themes/lucentui/js/app.js +++ b/themes/lucentui/js/app.js @@ -16,37 +16,86 @@ document.addEventListener('livewire:init', () => { Alpine.store('notifications', { notifications: [], - + init() { Livewire.on('notify', (e) => { this.addNotification(e); }); }, - + addNotification(notification) { notification = notification[0]; notification.show = false; notification.id = Date.now() + Math.floor(Math.random() * 1000); + notification.type = notification.type || 'success'; + notification.actions = Array.isArray(notification.actions) ? notification.actions : []; + notification.position = notification.position || 'bottom-right'; + notification.timeout = notification.timeout || 5000; + notification.paused = false; + notification._remaining = notification.timeout; + notification._startedAt = Date.now(); this.notifications.push(notification); requestAnimationFrame(() => { this.notifications = this.notifications.map(n => { - if (n.id === notification.id) { - n.show = true; - } + if (n.id === notification.id) n.show = true; return n; }); }); - setTimeout(() => { - this.removeNotification(notification.id); - }, notification.timeout || 5000); + this._scheduleDismiss(notification); }, - + + _scheduleDismiss(n) { + if (n._timer) clearTimeout(n._timer); + n._startedAt = Date.now(); + n._timer = setTimeout(() => { + if (!n.paused) this.removeNotification(n.id); + }, n._remaining); + }, + + pauseNotification(id) { + const n = this.notifications.find(x => x.id === id); + if (!n || n.paused) return; + n.paused = true; + n._remaining -= (Date.now() - n._startedAt); + if (n._timer) { clearTimeout(n._timer); n._timer = null; } + }, + + resumeNotification(id) { + const n = this.notifications.find(x => x.id === id); + if (!n || !n.paused) return; + n.paused = false; + this._scheduleDismiss(n); + }, + + invokeAction(id, actionIdx) { + const n = this.notifications.find(x => x.id === id); + if (!n) return; + const action = n.actions?.[actionIdx]; + if (!action) return; + if (action.event) { + const detail = { notificationId: id, payload: action.payload || null }; + window.dispatchEvent(new CustomEvent(action.event, { detail })); + if (window.Livewire && typeof window.Livewire.dispatch === 'function') { + try { window.Livewire.dispatch(action.event, [detail]); } catch (_) {} + } + } + if (action.href) { + if (window.Livewire && typeof window.Livewire.navigate === 'function') { + window.Livewire.navigate(action.href); + } else { + window.location.href = action.href; + } + } + if (action.dismiss !== false) this.removeNotification(id); + }, + removeNotification(id) { this.notifications = this.notifications.filter(n => { if (n.id === id) { n.show = false; + if (n._timer) clearTimeout(n._timer); } return n.show; }); diff --git a/themes/lucentui/views/components/button/danger.blade.php b/themes/lucentui/views/components/button/danger.blade.php index 86d9df2f1..b02ff8867 100644 --- a/themes/lucentui/views/components/button/danger.blade.php +++ b/themes/lucentui/views/components/button/danger.blade.php @@ -1,5 +1,5 @@
{{ $content }} diff --git a/themes/lucentui/views/components/form/checkbox.blade.php b/themes/lucentui/views/components/form/checkbox.blade.php index dbaf09f2d..55267212d 100644 --- a/themes/lucentui/views/components/form/checkbox.blade.php +++ b/themes/lucentui/views/components/form/checkbox.blade.php @@ -8,10 +8,10 @@
except(['label', 'name', 'id', 'class', 'divClass', 'required']) }} - class="peer form-checkbox size-5 text-primary rounded border-2 border-neutral bg-background-secondary/50 - focus:ring-2 focus:ring-primary/30 focus:ring-offset-0 focus:outline-none - checked:bg-primary checked:border-primary - hover:border-primary/50 transition-all duration-200 cursor-pointer + class="peer form-checkbox size-5 text-primary rounded border-2 border-neutral/70 bg-background-secondary/50 + focus:ring-2 focus:ring-primary/40 focus:ring-offset-0 focus:outline-none + checked:bg-primary checked:border-primary checked:shadow-[0_0_12px_-2px_hsl(var(--color-primary)/0.55)] + hover:border-primary/60 transition-all duration-200 cursor-pointer appearance-none" /> @@ -26,6 +26,6 @@ @error($name) -

{{ $message }}

+

{{ $message }}

@enderror
diff --git a/themes/lucentui/views/components/form/input.blade.php b/themes/lucentui/views/components/form/input.blade.php index ce8e115eb..06c2faeee 100644 --- a/themes/lucentui/views/components/form/input.blade.php +++ b/themes/lucentui/views/components/form/input.blade.php @@ -6,13 +6,13 @@ class="text-sm font-bold text-base"> {{ $label }} @if ($required && !$hideRequiredIndicator) - * + * @endif @endif except(['placeholder', 'label', 'id', 'name', 'type', 'class', 'divClass', 'required', 'hideRequiredIndicator', 'dirty']) }} @required($required) /> diff --git a/themes/lucentui/views/components/form/radio.blade.php b/themes/lucentui/views/components/form/radio.blade.php index bf4563ed7..8bba0d94f 100644 --- a/themes/lucentui/views/components/form/radio.blade.php +++ b/themes/lucentui/views/components/form/radio.blade.php @@ -14,7 +14,7 @@ @@ -30,10 +30,10 @@
@@ -46,6 +46,6 @@
@error($name) -

{{ $message }}

+

{{ $message }}

@enderror \ No newline at end of file diff --git a/themes/lucentui/views/components/form/select.blade.php b/themes/lucentui/views/components/form/select.blade.php index 1724ede70..a1d236a6a 100644 --- a/themes/lucentui/views/components/form/select.blade.php +++ b/themes/lucentui/views/components/form/select.blade.php @@ -15,7 +15,7 @@ class="text-sm font-bold text-base"> {{ $label }} @if ($required && !$hideRequiredIndicator) - * + * @endif @@ -41,6 +41,6 @@ @endif @error($name) -

{{ $message }}

+

{{ $message }}

@enderror \ No newline at end of file diff --git a/themes/lucentui/views/components/form/textarea.blade.php b/themes/lucentui/views/components/form/textarea.blade.php index 54b9ccc62..5299b6492 100644 --- a/themes/lucentui/views/components/form/textarea.blade.php +++ b/themes/lucentui/views/components/form/textarea.blade.php @@ -17,7 +17,7 @@ class="text-sm text-primary-100 absolute -translate-y-1/2 start-1 ml-1 bg-primary-800 px-2 rounded-md"> {{ $label }} @if ($required && !$hideRequiredIndicator) - * + * @endif @@ -28,6 +28,6 @@ wire:dirty.class="!border-yellow-600" @endif {{ $attributes->except(['placeholder', 'label', 'id', 'name', 'type', 'class', 'divClass', 'required', 'hideRequiredIndicator', 'dirty']) }} @required($required)>{{ $slot }} @error($name) -

{{ $message }}

+

{{ $message }}

@enderror \ No newline at end of file diff --git a/themes/lucentui/views/components/modal.blade.php b/themes/lucentui/views/components/modal.blade.php index 4e9b72c4f..8bbeb8dc1 100644 --- a/themes/lucentui/views/components/modal.blade.php +++ b/themes/lucentui/views/components/modal.blade.php @@ -17,18 +17,18 @@ x-transition:leave-end="opacity-0"> {{-- Backdrop overlay --}} -
{{-- Modal content --}} -

{{ $title }}

diff --git a/themes/lucentui/views/components/navigation/index.blade.php b/themes/lucentui/views/components/navigation/index.blade.php index 898dc880d..512aece2c 100644 --- a/themes/lucentui/views/components/navigation/index.blade.php +++ b/themes/lucentui/views/components/navigation/index.blade.php @@ -85,10 +85,10 @@ $watch('slideOverOpen', value => { document.documentElement.style.overflow = value ? 'hidden' : '' }); " :class="{ - 'shadow-lg shadow-black/5 border-neutral/80': scrolled, - 'border-neutral': !scrolled + 'shadow-elevated border-primary/20 backdrop-blur-xl bg-background-secondary/70': scrolled, + 'border-neutral bg-background-secondary': !scrolled }" - class="w-full px-4 lg:px-8 bg-background-secondary border-b flex flex-col lg:flex-row justify-between fixed top-0 z-20 h-auto transition-all duration-300"> + class="w-full px-4 lg:px-8 border-b flex flex-col lg:flex-row justify-between fixed top-0 z-20 h-auto transition-all duration-300">
@@ -135,6 +135,16 @@ + + +