Fix toasts

This commit is contained in:
Mahad Kalam 2026-02-09 22:42:13 +00:00
parent a897fcb7c0
commit ed75919d75
8 changed files with 130 additions and 11 deletions

View file

@ -87,6 +87,55 @@ select {
border-color: var(--color-green);
}
.flash-message {
@apply rounded-md text-center text-sm border px-4 py-3;
will-change: opacity, transform;
}
.flash-message--success {
color: var(--color-success);
border-color: var(--color-success);
background-color: rgba(51, 214, 166, 0.2);
background-color: color-mix(in oklab, var(--color-success) 20%, transparent);
}
.flash-message--error {
color: var(--color-danger);
border-color: var(--color-danger);
background-color: rgba(200, 57, 79, 0.2);
background-color: color-mix(in oklab, var(--color-danger) 20%, transparent);
}
.flash-message--enter {
animation: flash-enter 180ms ease-out;
}
.flash-message--leaving {
animation: flash-exit 240ms ease-in forwards;
}
@keyframes flash-enter {
from {
opacity: 0;
transform: translateY(-4px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes flash-exit {
from {
opacity: 1;
transform: translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateY(-6px) scale(0.98);
}
}
.loading {
@apply grayscale;
}

View file

@ -38,11 +38,20 @@ class InertiaController < ApplicationController
flash.to_hash.map do |type, message|
{
message: message.to_s,
class_name: type.to_sym == :notice ? "border-green text-green" : "border-primary text-primary"
class_name: flash_class_for(type)
}
end
end
def flash_class_for(type)
case type.to_sym
when :notice, :success
"flash-message--success"
else
"flash-message--error"
end
end
def inertia_primary_links
links = []
links << inertia_link("Home", root_path, active: helpers.current_page?(root_path))

View file

@ -0,0 +1,26 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { timeout: Number, exitDuration: Number }
connect() {
const timeout = this.hasTimeoutValue ? this.timeoutValue : 6000
this.exitDuration = this.hasExitDurationValue ? this.exitDurationValue : 250
this.element.classList.add("flash-message--enter")
this.hideTimeoutId = setTimeout(() => this.dismiss(), timeout)
}
disconnect() {
if (this.hideTimeoutId) clearTimeout(this.hideTimeoutId)
if (this.removeTimeoutId) clearTimeout(this.removeTimeoutId)
}
dismiss() {
if (this.leaving) return
this.leaving = true
this.element.classList.add("flash-message--leaving")
this.removeTimeoutId = setTimeout(() => {
this.element.remove()
}, this.exitDuration)
}
}

View file

@ -72,6 +72,10 @@
let navOpen = $state(false);
let logoutOpen = $state(false);
let currentlyExpanded = $state(false);
let flashVisible = $state(layout.nav.flash.length > 0);
let flashHiding = $state(false);
const flashHideDelay = 6000;
const flashExitDuration = 250;
const toggleNav = () => (navOpen = !navOpen);
const closeNav = () => (navOpen = false);
@ -116,6 +120,30 @@
if (isBrowser) document.body.classList.toggle("overflow-hidden", navOpen);
});
$effect(() => {
if (!layout.nav.flash.length) {
flashVisible = false;
flashHiding = false;
return;
}
flashVisible = true;
flashHiding = false;
let removeTimeoutId: ReturnType<typeof setTimeout> | undefined;
const hideTimeoutId = setTimeout(() => {
flashHiding = true;
removeTimeoutId = setTimeout(() => {
flashVisible = false;
flashHiding = false;
}, flashExitDuration);
}, flashHideDelay);
return () => {
clearTimeout(hideTimeoutId);
if (removeTimeoutId) clearTimeout(removeTimeoutId);
};
});
onMount(() => {
if (!isBrowser) return;
handleResize();
@ -134,13 +162,13 @@
`block px-3 py-2 rounded-md text-sm transition-colors ${active ? "bg-primary text-white" : "hover:bg-darkless"}`;
</script>
{#if layout.nav.flash.length > 0}
{#if flashVisible && layout.nav.flash.length > 0}
<div
class="fixed top-4 left-1/2 transform -translate-x-1/2 z-50 w-full max-w-md px-4 space-y-2"
>
{#each layout.nav.flash as item}
<div
class={`rounded-md text-center text-sm px-4 py-3 shadow-lg ${item.class_name}`}
class={`flash-message shadow-lg flash-message--enter ${flashHiding ? "flash-message--leaving" : ""} ${item.class_name}`}
>
{item.message}
</div>

View file

@ -25,13 +25,13 @@
<main class="flex-1 lg:ml-[250px] lg:max-w-[calc(100%-250px)] p-5 mb-[100px] pt-16 lg:pt-5 transition-all duration-300 ease-in-out">
<%- if flash[:notice].present? %>
<div class="mb-6 p-4 bg-green-500/10 border border-green-500/20 rounded-lg text-green-400">
<div class="flash-message flash-message--enter flash-message--success mb-6" data-controller="flash">
<%= flash[:notice] %>
</div>
<% end -%>
<%- if flash[:alert].present? %>
<div class="mb-6 p-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400">
<div class="flash-message flash-message--enter flash-message--error mb-6" data-controller="flash">
<%= flash[:alert] %>
</div>
<% end -%>

View file

@ -8,15 +8,22 @@
<%= stylesheet_link_tag 'tailwind', 'data-turbo-track': 'reload' %>
<%= csrf_meta_tags %>
<%= javascript_importmap_tags %>
</head>
<body class="min-h-screen bg-darker text-white" style="margin: 0; padding: 0; display: flex; align-items: center; justify-content: center;">
<main class="w-full max-w-md px-5 py-8">
<%- if flash[:notice].present? %>
<div class="mb-6 p-4 bg-green-500/10 border border-green-500/20 rounded-lg text-green-400">
<div class="flash-message flash-message--enter flash-message--success mb-6" data-controller="flash">
<%= flash[:notice] %>
</div>
<% end -%>
<%- if flash[:alert].present? %>
<div class="flash-message flash-message--enter flash-message--error mb-6" data-controller="flash">
<%= flash[:alert] %>
</div>
<% end -%>
<%= yield %>
</main>
</body>

View file

@ -6,7 +6,7 @@
<p class="text-gray-400 mb-6">We try to autodetect your Git repository, but you can manually specify it if needed.</p>
<% if flash[:alert] %>
<div class="bg-red-900/50 border border-red-500 text-red-300 px-4 py-3 rounded-lg mb-4">
<div class="flash-message flash-message--enter flash-message--error mb-4" data-controller="flash">
<%= flash[:alert] %>
</div>
<% end %>

View file

@ -4,13 +4,13 @@
<%
c =
case name.to_sym
when :notice
'border-green text-green'
when :notice, :success
'flash-message--success'
else
'border-primary text-primary'
'flash-message--error'
end
%>
<div class="rounded-md text-center text-sm px-3 py-2 <%= c %>"><%= msg %></div>
<div class="flash-message flash-message--enter <%= c %>" data-controller="flash"><%= msg %></div>
<% end %>
<% if current_user %>