From 08ee9018cff24e3bbc9a1979fe5d12785071015e Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 10 May 2026 16:44:30 +0200 Subject: [PATCH] website add toast service for notifications --- rust/src_webpack/src/main.ts | 1 + rust/src_webpack/src/toast.ts | 94 +++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 rust/src_webpack/src/toast.ts diff --git a/rust/src_webpack/src/main.ts b/rust/src_webpack/src/main.ts index ba8c957..69845dc 100644 --- a/rust/src_webpack/src/main.ts +++ b/rust/src_webpack/src/main.ts @@ -31,6 +31,7 @@ import { FileList, SolarState, PumpTestResult } from "./api"; import {SolarView} from "./solarview"; +import {toast} from "./toast"; export class Controller { loadTankInfo(): Promise { diff --git a/rust/src_webpack/src/toast.ts b/rust/src_webpack/src/toast.ts new file mode 100644 index 0000000..bca6b71 --- /dev/null +++ b/rust/src_webpack/src/toast.ts @@ -0,0 +1,94 @@ +class ToastService { + private container: HTMLElement; + private stylesInjected = false; + + constructor() { + this.container = this.ensureContainer(); + this.injectStyles(); + } + + info(message: string, timeoutMs: number = 5000) { + const el = this.createToast(message, 'info'); + this.container.appendChild(el); + // Auto-dismiss after timeout + const timer = window.setTimeout(() => this.dismiss(el), timeoutMs); + // Dismiss on click immediately + el.addEventListener('click', () => { + window.clearTimeout(timer); + this.dismiss(el); + }); + } + + error(message: string) { + console.error(message); + const el = this.createToast(message, 'error'); + this.container.appendChild(el); + // Only dismiss on click + el.addEventListener('click', () => this.dismiss(el)); + } + + private dismiss(el: HTMLElement) { + if (!el.parentElement) return; + el.parentElement.removeChild(el); + } + + private createToast(message: string, type: 'info' | 'error'): HTMLElement { + const div = document.createElement('div'); + div.className = `toast ${type}`; + div.textContent = message; + div.setAttribute('role', 'status'); + div.setAttribute('aria-live', 'polite'); + return div; + } + + private ensureContainer(): HTMLElement { + let container = document.getElementById('toast-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'toast-container'; + document.body.appendChild(container); + } + return container; + } + + private injectStyles() { + if (this.stylesInjected) return; + const style = document.createElement('style'); + style.textContent = ` +#toast-container { + position: fixed; + top: 12px; + right: 12px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 9999; +} +.toast { + max-width: 320px; + padding: 10px 12px; + border-radius: 6px; + box-shadow: 0 2px 6px rgba(0,0,0,0.2); + cursor: pointer; + user-select: none; + font-family: sans-serif; + font-size: 14px; + line-height: 1.3; +} +.toast.info { + background-color: #d4edda; /* green-ish */ + color: #155724; + border-left: 4px solid #28a745; +} +.toast.error { + background-color: #f8d7da; /* red-ish */ + color: #721c24; + border-left: 4px solid #dc3545; +} +`; + document.head.appendChild(style); + this.stylesInjected = true; + } +} + +export const toast = new ToastService();