/** * Publishes the top-most mounted AppTopBar's bottom edge as `--mint-toast-offset-top` * on ``, so the teleported toast container starts below the top bar. */ const TOAST_OFFSET_PROPERTY = '--mint-toast-offset-top' const LISTENER_OPTIONS = { capture: true, passive: true } as const const topBars = new Set() let observer: ResizeObserver | undefined function syncToastOffset(): void { const style = document.documentElement.style if (topBars.size === 0) { style.removeProperty(TOAST_OFFSET_PROPERTY) return } const header = [...topBars] .map((el) => el.getBoundingClientRect()) .reduce((top, rect) => (rect.top < top.top ? rect : top)) // Clamp so a top bar scrolled out of view leaves toasts at the plain 1rem gap. style.setProperty(TOAST_OFFSET_PROPERTY, `calc(${Math.max(0, Math.round(header.bottom))}px + 1rem)`) } /** Track a top bar element; returns the cleanup to call on unmount. */ export function trackTopBarForToasts(el: HTMLElement): () => void { if (topBars.size === 0) { // Capture so scrolling any container that holds the top bar also moves the toasts. window.addEventListener('scroll', syncToastOffset, LISTENER_OPTIONS) window.addEventListener('resize', syncToastOffset, LISTENER_OPTIONS) if (typeof ResizeObserver !== 'undefined') observer = new ResizeObserver(syncToastOffset) } topBars.add(el) observer?.observe(el) syncToastOffset() return () => { topBars.delete(el) observer?.unobserve(el) if (topBars.size === 0) { window.removeEventListener('scroll', syncToastOffset, LISTENER_OPTIONS) window.removeEventListener('resize', syncToastOffset, LISTENER_OPTIONS) observer?.disconnect() observer = undefined } syncToastOffset() } }