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

View File

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

View File

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

View File

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

View File

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