1
0

fix: toast visibility — opaque backgrounds and flash scope matching with query strings

Two bugs in the toast redesign:

1. Variant backgrounds (success/warning/info/error) used rgba with 12-20%
   opacity, making toasts semi-transparent over page content. Fixed by
   layering the tint over a solid background via linear-gradient compositing.

2. Flash messages with scoped paths including query strings (e.g.
   edit pages with ?return=...) were not matched by Flash::peek(Request::path())
   since path() strips the query. Added fallback to pathWithQuery() matching.

Also moved flash partial before JS init scripts to ensure DOM is present
when the component runtime mounts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 19:58:15 +01:00
parent c89501bbed
commit 1ba6829a1c
5 changed files with 44 additions and 54 deletions

View File

@@ -145,7 +145,7 @@ $componentPageConfig = [
'components' => [
'confirmActions' => [],
'flashAutoDismiss' => [
'selector' => '.app-toast-stack .notice[data-flash-timeout]',
'selector' => '.notice[data-flash-timeout]',
],
'passwordHints' => [],
'copyBadge' => [],
@@ -358,8 +358,8 @@ $componentPageConfig['moduleRuntimeComponents'] = $moduleRuntimeComponents;
?>
<?php endforeach; ?>
<script type="application/json" id="page-config-app-components"><?php gridJsonForJs($componentPageConfig); ?></script>
<script type="module" src="<?php e(assetVersion('js/app-init.js')); ?>"></script>
<?php require __DIR__ . '/partials/app-flash.phtml'; ?>
<script type="module" src="<?php e(assetVersion('js/app-init.js')); ?>"></script>
</body>
</html>

View File

@@ -15,7 +15,7 @@ if ($bufferTitle !== '') {
$loginComponentConfig = [
'components' => [
'flashAutoDismiss' => [
'selector' => '.app-toast-stack .notice[data-flash-timeout]',
'selector' => '.notice[data-flash-timeout]',
],
'passwordHints' => [
'selector' => '[data-password-hints]',
@@ -65,8 +65,8 @@ $loginComponentConfig = [
<?php require __DIR__ . '/partials/app-footer.phtml'; ?>
<div id="gradient"></div>
<script type="application/json" id="page-config-login-components"><?php gridJsonForJs($loginComponentConfig); ?></script>
<script type="module" src="<?php e(assetVersion('js/app-login-init.js')); ?>"></script>
<?php require __DIR__ . '/partials/app-flash.phtml'; ?>
<script type="module" src="<?php e(assetVersion('js/app-login-init.js')); ?>"></script>
</body>
</html>

View File

@@ -4,7 +4,7 @@ use MintyPHP\Support\Flash;
use MintyPHP\Http\Request;
use MintyPHP\Session;
$messages = Flash::peek(Request::path());
$messages = Flash::peek(Request::path()) ?: Flash::peek(Request::pathWithQuery());
if (!$messages) {
return;
}

View File

@@ -14,6 +14,10 @@
pointer-events: none;
}
.app-toast-stack:empty {
display: none;
}
/* In admin layout: offset below topbar */
.app-container ~ .app-toast-stack,
body:has(.app-container) .app-toast-stack {
@@ -37,14 +41,16 @@
/* ── Individual toast notice ─────────────────────────────────────────── */
.notice {
.app-toast-stack .notice {
--app-notice-border-color: var(--app-muted-border-color);
--app-notice-background-color: var(--app-card-background-color);
--app-notice-background-color: var(--app-background-color);
--app-notice-accent-color: var(--app-color);
background: var(--app-notice-background-color);
--_toast-tint: transparent;
background: linear-gradient(var(--_toast-tint), var(--_toast-tint)), var(--app-background-color);
border: 1px solid color-mix(in srgb, var(--app-notice-border-color) 40%, transparent);
border-left: 3px solid var(--app-notice-border-color);
border-radius: var(--app-border-radius);
box-shadow: var(--app-card-box-shadow, 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.06));
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 1px 3px rgba(0, 0, 0, 0.08);
padding: calc(var(--app-spacing) * 0.625) calc(var(--app-spacing) * 0.875);
padding-right: calc(var(--app-spacing) * 0.5);
display: flex;
@@ -57,80 +63,64 @@
font-size: 0.875rem;
line-height: 1.4;
pointer-events: auto;
}
/* ── Slide-in animation ──────────────────────────────────────────────── */
.notice {
margin: 0;
animation: toast-slide-in 250ms ease-out;
}
.notice.toast-dismissing {
.app-toast-stack .notice.toast-dismissing {
animation: toast-slide-out 200ms ease-in forwards;
}
@keyframes toast-slide-in {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes toast-slide-out {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(20px);
}
from { opacity: 1; transform: translateX(0); }
to { opacity: 0; transform: translateX(20px); }
}
@media (prefers-reduced-motion: reduce) {
.notice,
.notice.toast-dismissing {
.app-toast-stack .notice,
.app-toast-stack .notice.toast-dismissing {
animation: none;
}
}
/* ── Variant colors ──────────────────────────────────────────────────── */
.notice strong {
.app-toast-stack .notice strong {
color: var(--app-notice-accent-color);
}
.notice[data-variant=success] {
.app-toast-stack .notice[data-variant=success] {
--app-notice-border-color: var(--app-notice-success-border-color);
--app-notice-background-color: var(--app-notice-success-background-color);
--_toast-tint: var(--app-notice-success-background-color);
--app-notice-accent-color: var(--app-notice-success-color);
}
.notice[data-variant=warning] {
.app-toast-stack .notice[data-variant=warning] {
--app-notice-border-color: var(--app-notice-warning-border-color);
--app-notice-background-color: var(--app-notice-warning-background-color);
--_toast-tint: var(--app-notice-warning-background-color);
--app-notice-accent-color: var(--app-notice-warning-color);
}
.notice[data-variant=info] {
.app-toast-stack .notice[data-variant=info] {
--app-notice-border-color: var(--app-notice-info-border-color);
--app-notice-background-color: var(--app-notice-info-background-color);
--_toast-tint: var(--app-notice-info-background-color);
--app-notice-accent-color: var(--app-notice-info-color);
}
.notice[data-variant=error] {
.app-toast-stack .notice[data-variant=error] {
--app-notice-border-color: var(--app-notice-error-border-color);
--app-notice-background-color: var(--app-notice-error-background-color);
--_toast-tint: var(--app-notice-error-background-color);
--app-notice-accent-color: var(--app-notice-error-color);
}
/* ── Progress bar timer ──────────────────────────────────────────────── */
.notice.flash-timed::after {
.app-toast-stack .notice.flash-timed::after {
content: "";
position: absolute;
left: 0;
@@ -144,7 +134,7 @@
opacity: 0.7;
}
.notice.flash-timed:hover::after {
.app-toast-stack .notice.flash-timed:hover::after {
animation-play-state: paused;
}
@@ -154,42 +144,42 @@
}
@media (prefers-reduced-motion: reduce) {
.notice.flash-timed::after {
.app-toast-stack .notice.flash-timed::after {
animation: none;
}
}
/* ── Dismiss button ──────────────────────────────────────────────────── */
.notice button[type=submit],
.notice button[type=button] {
.app-toast-stack .notice button[type=submit],
.app-toast-stack .notice button[type=button] {
border: none !important;
background: transparent;
color: var(--app-notice-border-color);
opacity: 0.7;
opacity: 0.6;
cursor: pointer;
padding: 2px 4px;
line-height: 1;
flex-shrink: 0;
}
.notice button[type=submit]:hover,
.notice button[type=button]:hover {
.app-toast-stack .notice button[type=submit]:hover,
.app-toast-stack .notice button[type=button]:hover {
opacity: 1;
}
/* ── List formatting inside notices ──────────────────────────────────── */
.notice ul {
.app-toast-stack .notice ul {
margin: 0;
padding-left: 0;
}
.notice li {
.app-toast-stack .notice li {
list-style: none;
}
.notice li:last-child {
.app-toast-stack .notice li:last-child {
margin: 0;
}
}

View File

@@ -6,7 +6,7 @@ import { resolveHost } from '../core/app-dom.js';
export function initFlashAutoDismiss(root = document, options = {}) {
const {
selector = '.app-toast-stack .notice[data-flash-timeout]',
selector = '.notice[data-flash-timeout]',
defaultTimeout = 0,
} = options;
const host = resolveHost(root);