import { css, html } from "lit"; import { BaseElement } from "../../core/base-element.ts"; /** * CFToastProvider - Fixed-position container that manages a stack of cf-toast elements * * @element cf-toast-provider * * @attr {string} position - Screen position: "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right" * @attr {number} max - Maximum number of simultaneously visible toasts * * @slot - cf-toast elements to manage * * @cssprop --cf-toast-provider-gap - Vertical gap between toasts (default: 0.5rem) * @cssprop --cf-toast-provider-margin - Inset from screen edge (default: 1rem) * @cssprop --cf-toast-provider-z-index - Z-index of fixed container (default: 1100) */ export class CFToastProvider extends BaseElement { static override styles = css` :host { display: block; position: fixed; z-index: var(--cf-toast-provider-z-index, 1100); pointer-events: none; } .container { display: flex; flex-direction: column; gap: var(--cf-toast-provider-gap, 0.5rem); pointer-events: auto; padding: var(--cf-toast-provider-margin, 1rem); } /* bottom (default): centered horizontally, stack upward */ :host([position="bottom"]), :host(:not([position])) { bottom: 0; left: 50%; transform: translateX(-50%); } :host([position="bottom"]) .container, :host(:not([position])) .container { flex-direction: column-reverse; } /* top: centered horizontally, stack downward */ :host([position="top"]) { top: 0; left: 50%; transform: translateX(-50%); } /* bottom-right */ :host([position="bottom-right"]) { bottom: 0; right: 0; } :host([position="bottom-right"]) .container { flex-direction: column-reverse; align-items: flex-end; } /* bottom-left */ :host([position="bottom-left"]) { bottom: 0; left: 0; } :host([position="bottom-left"]) .container { flex-direction: column-reverse; align-items: flex-start; } /* top-right */ :host([position="top-right"]) { top: 0; right: 0; } :host([position="top-right"]) .container { align-items: flex-end; } /* top-left */ :host([position="top-left"]) { top: 0; left: 0; } :host([position="top-left"]) .container { align-items: flex-start; } ::slotted(cf-toast) { pointer-events: auto; } `; static override properties = { position: { type: String, reflect: true }, max: { type: Number }, }; declare position: | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right"; declare max: number; private _observer: MutationObserver | null = null; constructor() { super(); this.position = "bottom"; this.max = 3; } override connectedCallback(): void { super.connectedCallback(); this._observer = new MutationObserver((mutations) => { const relevant = mutations.some( (m) => m.type === "childList" || (m.type === "attributes" && m.attributeName === "open"), ); if (relevant) { this._updateStack(); } }); this._observer.observe(this, { childList: true, attributes: true, attributeFilter: ["open"], subtree: true, }); this._updateStack(); } override disconnectedCallback(): void { super.disconnectedCallback(); if (this._observer) { this._observer.disconnect(); this._observer = null; } } private _updateStack(): void { const openToasts = Array.from( this.querySelectorAll("cf-toast[open]"), ); if (openToasts.length > this.max) { const toEvict = openToasts.slice(0, openToasts.length - this.max); for (const toast of toEvict) { toast.removeAttribute("open"); toast.dispatchEvent( new CustomEvent("cf-toast-dismiss", { detail: { reason: "timeout" }, bubbles: true, composed: true, }), ); } } } override render() { return html`
`; } }