refactor(ui): move toast stack chrome to a server-rendered partial

Mirrors the detail-drawer move: the .app-toast-stack container is no
longer lazy-created in JS on first toast, instead it lives in
templates/partials/app-toast-stack.phtml and is mounted once in
default.phtml alongside the confirm dialog. The aria-live attribute now
sits in the SSR markup so screen readers register the live region from
page load, not from the first toast.

getToastStack() drops to a plain querySelector and warns via warnOnce
when the partial is missing instead of constructing a fallback container.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 19:13:55 +02:00
parent 4d1bd13a3b
commit 2429588256
3 changed files with 30 additions and 12 deletions

View File

@@ -2,8 +2,12 @@ import { warnOnce } from '../core/app-dom.js';
/**
* Async Flash Messages & Loading State
* Renders flash messages into the .app-toast-stack for JS-triggered notifications.
* Creates the stack container on demand if it doesn't exist yet.
*
* Renders flash messages into the .app-toast-stack for JS-triggered
* notifications. The stack container is rendered server-side by
* templates/partials/app-toast-stack.phtml (mounted once in default.phtml);
* this module only appends/removes toasts inside it.
*
* Provides loading cursor state management.
*/
@@ -43,18 +47,13 @@ const defaultTimeouts = {
const MAX_VISIBLE_TOASTS = 5;
/**
* Get or create the toast stack container.
* @returns {HTMLElement}
* Resolve the server-rendered toast stack container.
* Returns null if the partial is missing — callers must handle that case.
* @returns {HTMLElement|null}
*/
function getToastStack() {
let stack = document.querySelector('.app-toast-stack');
if (!stack) {
stack = document.createElement('div');
stack.className = 'app-toast-stack';
stack.setAttribute('aria-live', 'polite');
document.body.appendChild(stack);
}
return stack;
const stack = document.querySelector('.app-toast-stack');
return stack instanceof HTMLElement ? stack : null;
}
/**
@@ -95,6 +94,12 @@ function enforceMaxVisible(stack) {
*/
export function showAsyncFlash(type, message, timeout) {
const stack = getToastStack();
if (!stack) {
warnOnce('UI_EL_MISSING', 'Missing .app-toast-stack — partial not mounted?', {
module: 'async-flash',
});
return null;
}
const effectiveTimeout = timeout ?? defaultTimeouts[type] ?? 5000;