BUG-05: Speichern-Button weg, Save-Status-Indikator mit States

Der 'Speichern'-Button war redundant — debouncedSave() lief schon
bei jedem Drop/Resize/Add/Delete. Klick auf den Button machte nur
einen sofortigen POST statt 500ms-debounce. Verwirrend statt hilfreich.

Fix:
- Speichern-Button aus dem Toolbar entfernt.
- Stattdessen: <div id='saveIndicator' data-state='idle'> mit .save-dot
  und .save-label — rechtsbündig im Toolbar (margin-left:auto).
- States: idle | pending | saving | error (CSS-Data-Attr-basiert).
  - idle: grüner Punkt, 'Gespeichert'
  - pending: warn-gelber Punkt pulsiert, 'Ungespeichert…' (debouncedSave-Start)
  - saving: info-blauer Punkt pulsiert schneller, 'Speichern…' (saveLayout-Start)
  - error: alert-roter Punkt, 'Fehler: ...' (j.ok=false)
- setSaveState(state, label?) als zentraler Helper.
- saveLayout() macht jetzt success-toast weg — der Indicator reicht.
- Fehlerfall: error-toast bleibt (kritisch, separat sichtbar).

Vorteile:
- Toolbar ist aufgeräumter, +Button ist eindeutig der primäre Action.
- User sieht Save-Status ohne Klick auf irgendwas — visuelles Feedback
  für Auto-Save.
- Fehler-State zeigt sofort 'Fehler: ...' mit Details.

Beweis: tests/test_save_indicator.js (11/11 grün)
  1. Speichern-Button entfernt
  2. Save-Indicator #saveIndicator vorhanden
  3. setSaveState() Funktion definiert
  4. setSaveState setzt dataset.state
  5. debouncedSave setzt pending-Status
  6. saveLayout setzt saving am Anfang
  7. saveLayout setzt idle bei Erfolg
  8. saveLayout setzt error bei j.ok=false
  9. CSS für alle 4 Indicator-States
 10. Kein success-toast in saveLayout mehr
 11. Save-Indicator rechtsbündig (margin-left:auto)

Closes #7
This commit is contained in:
ki
2026-08-29 17:47:23 +04:00
parent 5f74238e0e
commit d0cc69410f
2 changed files with 155 additions and 14 deletions
+81 -14
View File
@@ -330,6 +330,38 @@
display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
margin-bottom: 16px;
}
/* BUG-05: Save-Status-Indikator (ersetzt den redundanten Speichern-Button) */
.save-indicator {
display: inline-flex; align-items: center; gap: 6px;
margin-left: auto; /* rechtsbündig im toolbar */
padding: 6px 10px;
border-radius: 6px;
font-size: 0.82em;
color: var(--fg-muted);
transition: color 0.15s ease;
}
.save-indicator .save-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 6px rgba(74, 222, 128, 0.5);
transition: background 0.15s ease, box-shadow 0.15s ease;
}
/* Zustände */
.save-indicator[data-state="idle"] .save-dot { background: var(--ok); }
.save-indicator[data-state="pending"] .save-dot {
background: var(--warn); animation: savePulse 1s ease-in-out infinite;
}
.save-indicator[data-state="saving"] .save-dot {
background: var(--info); animation: savePulse 0.6s ease-in-out infinite;
}
.save-indicator[data-state="error"] .save-dot {
background: var(--alert); box-shadow: 0 0 6px rgba(248, 113, 113, 0.5);
}
.save-indicator[data-state="error"] { color: var(--alert); }
@keyframes savePulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
.add-form {
display: flex; gap: 8px; align-items: center;
background: var(--surface-2);
@@ -1014,7 +1046,12 @@
<button type="submit" class="primary">+ Hinzufügen</button>
</form>
<button onclick="packAll()" title="Alle Items automatisch anordnen">Auto-Pack</button>
<button onclick="saveLayout()" class="primary" title="Layout auf Server speichern">Speichern</button>
<!-- BUG-05: Speichern-Button entfernt (redundant zu debouncedSave).
Stattdessen subtiler Save-Status-Indikator rechts. -->
<div class="save-indicator" id="saveIndicator" data-state="idle">
<span class="save-dot"></span>
<span class="save-label">Gespeichert</span>
</div>
</div>
<div id="gridPreview" class="grid-preview"></div>
<div class="layout-hint">
@@ -1244,8 +1281,50 @@
updateClock();
// ============ DRAG & DROP LAYOUT EDITOR ============
async function saveLayout() {
// BUG-05: Status-Indikator statt redundantem "Speichern"-Button.
// Status-Lebenszyklus:
// pending → saving → idle (success) | error
setSaveState('saving');
const r = await fetch('/api/layout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: layoutItems }),
});
const j = await r.json();
if (!j.ok) {
setSaveState('error', 'Fehler: ' + (j.error || '?'));
toast('Layout speichern fehlgeschlagen: ' + (j.error || '?'), 'error', 5000);
return;
}
layoutItems = j.items;
renderGrid();
setTimeout(refreshPreview, 500);
setSaveState('idle', 'Gespeichert');
// nicht mehr länger toasten — der Indicator reicht
}
// BUG-05: Save-Status-Indikator Helper
function setSaveState(state, label) {
const el = document.getElementById('saveIndicator');
if (!el) return;
el.dataset.state = state;
const lbl = el.querySelector('.save-label');
if (lbl && label) lbl.textContent = label;
// Idle-Reset: nach kurzer Anzeige "Gespeichert" zurück
if (state === 'saving' && !label) {
const lblEl = el.querySelector('.save-label');
if (lblEl) lblEl.textContent = 'Speichern…';
}
if (state === 'idle' && !label) {
const lblEl = el.querySelector('.save-label');
if (lblEl) lblEl.textContent = 'Gespeichert';
}
}
// BUG-05: debouncedSave setzt pending-Status
let saveTimer = null;
function debouncedSave() {
setSaveState('pending', 'Ungespeichert…');
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(saveLayout, 500);
}
@@ -1608,19 +1687,7 @@
renderGrid();
toast('Widget hinzugefügt', 'success', 2000);
}
async function saveLayout() {
const r = await fetch('/api/layout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: layoutItems }),
});
const j = await r.json();
if (!j.ok) { toast('Layout speichern fehlgeschlagen: ' + (j.error || '?'), 'error', 5000); return; }
layoutItems = j.items;
renderGrid();
setTimeout(refreshPreview, 500);
toast('Layout gespeichert', 'success');
}
// BUG-05: alte saveLayout entfernt — siehe oben (mit setSaveState).
async function packAll() {
const ok = await modalConfirm({
icon: 'warn',