diff --git a/templates/index.html b/templates/index.html index fdf7011..d5e1eb1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -805,6 +805,21 @@ .toast.info .toast-icon { color: var(--info); } .toast.warn .toast-icon { color: var(--warn); } .toast-text { flex: 1; min-width: 0; word-break: break-word; } + .toast-action { + flex: 0 0 auto; + padding: 4px 10px; + background: var(--surface-3); + border: 1px solid var(--border-light); + color: var(--fg); + border-radius: 4px; + font-size: 0.85em; + font-family: inherit; + cursor: pointer; + pointer-events: auto; /* BUG-06: Klick auf Action-Button muss trotz Toast-Click-Handler gehen */ + } + .toast-action:hover { background: var(--accent); color: white; border-color: var(--accent); } + .toast.success .toast-action { border-color: var(--success); color: var(--success); } + .toast.success .toast-action:hover { background: var(--success); color: var(--bg); } @keyframes toastIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } @@ -1143,12 +1158,25 @@ // ============ Toast-Helper ============ // Aufruf: toast("Item gelöscht", "success" | "error" | "info" | "warn") // Optional: toast(text, type, duration_ms) — duration 0 = manuell wegklicken - function toast(text, type = "info", duration = 3500) { + // Optional: toast(text, type, duration_ms, action={label, onClick}) + // — Toast wird mit action-Button gerendert, Click auf den Button ruft onClick + // auf und dismissed den Toast. + function toast(text, type = "info", duration = 3500, action = null) { const c = document.getElementById('toastContainer'); const el = document.createElement('div'); el.className = 'toast ' + type; const iconChar = { success: '✓', error: '✕', info: 'ⓘ', warn: '⚠' }[type] || 'ⓘ'; - el.innerHTML = `
${iconChar}
`; + if (action) { + el.innerHTML = `
${iconChar}
+
+ `; + el.querySelector('.toast-action').addEventListener('click', (e) => { + e.stopPropagation(); + try { action.onClick(); } finally { remove(); } + }); + } else { + el.innerHTML = `
${iconChar}
`; + } el.querySelector('.toast-text').textContent = text; let removed = false; const remove = () => { @@ -1703,17 +1731,41 @@ const ok = await modalConfirm({ icon: 'warn', title: 'Alle Items neu anordnen?', - body: 'Auto-Pack sortiert alle Widgets nach Größe in den 4×4-Grid. Bestehende manuelle Positionen gehen verloren.', + body: 'Auto-Pack sortiert alle Widgets nach Größe in den 4×4-Grid. Bestehende manuelle Positionen gehen verloren. Du kannst den Schritt 10 Sekunden lang rückgängig machen.', confirmText: 'Neu anordnen', }); if (!ok) return; + // BUG-06: Snapshot der aktuellen Items in sessionStorage für Undo. + // server gibt neue Items zurück; wir können das alte Layout wiederherstellen. + const snapshot = JSON.stringify(layoutItems); const r = await fetch('/api/layout/pack', { method: 'POST' }); const j = await r.json(); if (!j.ok) { toast('Auto-Pack fehlgeschlagen: ' + (j.error || '?'), 'error', 5000); return; } layoutItems = j.items; renderGrid(); debouncedSave(); - toast('Layout automatisch angeordnet', 'success'); + // BUG-06: Actionable Toast mit Undo-Button + toast('Layout automatisch angeordnet', 'success', 10000, { + label: 'Rückgängig', + onClick: async () => { + try { + const restored = JSON.parse(snapshot); + // restore via normalen Save-Endpunkt + const r2 = await fetch('/api/layout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ items: restored }), + }); + const j2 = await r2.json(); + if (!j2.ok) { toast('Undo fehlgeschlagen: ' + (j2.error || '?'), 'error', 5000); return; } + layoutItems = j2.items; + renderGrid(); + toast('Layout wiederhergestellt', 'info', 3000); + } catch (err) { + toast('Undo Fehler: ' + err.message, 'error', 5000); + } + } + }); } // ============ Plugin-Configs ============ diff --git a/tests/test_autopack_undo.js b/tests/test_autopack_undo.js new file mode 100644 index 0000000..6112916 --- /dev/null +++ b/tests/test_autopack_undo.js @@ -0,0 +1,58 @@ +// BUG-06 Test: Auto-Pack Undo via Snapshot +// +// Erwartung: +// 1) packAll() speichert layoutItems als JSON vor dem Pack +// 2) Toast wird mit action={label:"Rückgängig", onClick} aufgerufen (statt nur Text) +// 3) onClick sendet POST /api/layout mit den snapshot-Items +// 4) Modal-Text erwähnt "10 Sekunden lang rückgängig" +// 5) CSS: .toast-action Klasse vorhanden +// 6) toast()-Funktion unterstützt action-Parameter (4-Args-Signatur) + +const fs = require('fs'); +const html = fs.readFileSync('templates/index.html', 'utf8'); +const js = html.match(/