feat: improve setup page UX with SVG icons, edit state, and global max-width

Replace Unicode edit/delete characters with SVG pencil/trash icon buttons,
add edit mode visual feedback (orange ring + Save/Cancel buttons), group
action buttons consistently across all entity types, add hover effects to
list items, and constrain all pages to 1800px max-width via layout template.
This commit is contained in:
2026-02-09 15:24:51 +01:00
parent c23ac1611a
commit 277d1c949f
5 changed files with 123 additions and 24 deletions

View File

@@ -26,6 +26,7 @@
"timeout": "Zeit\u00fcberschreitung bei Standortabfrage."
},
"add": "Profil hinzuf\u00fcgen",
"save": "Profil speichern",
"edit": "Bearbeiten",
"delete": "L\u00f6schen",
"noItems": "Noch keine Profile. Erstellen Sie eines, um zu beginnen."
@@ -62,6 +63,7 @@
"tooltip": "Aktuelle oder gew\u00fcnschte Raumtemperatur. Standard 25\u00b0C."
},
"add": "Raum hinzuf\u00fcgen",
"save": "Raum speichern",
"noItems": "Noch keine R\u00e4ume. F\u00fcgen Sie R\u00e4ume zu Ihrem Profil hinzu."
},
"devices": {
@@ -75,6 +77,7 @@
"wattsPeak": { "label": "Watt (Spitze)", "tooltip": "Leistungsaufnahme bei Maximallast (z.B. Gaming)" },
"dutyCycle": { "label": "Einschaltdauer", "tooltip": "Anteil der aktiven Zeit (0\u20131). K\u00fchlschrank \u2248 0,3, PC \u2248 1,0." },
"add": "Ger\u00e4t hinzuf\u00fcgen",
"save": "Ger\u00e4t speichern",
"noItems": "Noch keine Ger\u00e4te."
},
"occupants": {
@@ -89,6 +92,7 @@
},
"vulnerable": { "label": "Schutzbed\u00fcrftig", "tooltip": "Ankreuzen bei \u00e4lteren Menschen, Kleinkindern oder gesundheitlich eingeschr\u00e4nkten Personen. F\u00fcgt Pflegeerinnerungen hinzu." },
"add": "Bewohner hinzuf\u00fcgen",
"save": "Bewohner speichern",
"noItems": "Noch keine Bewohner."
},
"ac": {
@@ -105,6 +109,7 @@
"dehumidify": { "label": "Entfeuchtung", "tooltip": "Ob das Ger\u00e4t einen Entfeuchtungsmodus hat" },
"rooms": { "label": "Zugewiesene R\u00e4ume", "tooltip": "Welche R\u00e4ume dieses Klimager\u00e4t versorgt" },
"add": "Klimager\u00e4t hinzuf\u00fcgen",
"save": "Klimager\u00e4t speichern",
"noItems": "Noch keine Klimager\u00e4te."
},
"toggles": {
@@ -173,7 +178,23 @@
"riskLow": "Niedrig",
"riskModerate": "Mittel",
"riskHigh": "Hoch",
"riskExtreme": "Extrem"
"riskExtreme": "Extrem",
"noActions": "Keine Maßnahmen",
"effort": "Aufwand",
"impact": "Wirkung",
"aiDisclaimer": "KI-generierte Zusammenfassung. Kein Ersatz für professionelle Beratung.",
"coolVentilate": "Fenster öffnen",
"coolAC": "Klimaanlage",
"coolOverloaded": "Klima überlastet",
"aiActions": "KI-empfohlene Maßnahmen",
"category": {
"shading": "Verschattung",
"ventilation": "Lüftung",
"internal_gains": "Wärmequellen",
"ac_strategy": "Klimastrategie",
"hydration": "Flüssigkeit",
"care": "Pflege"
}
},
"guide": {
"title": "Erste Schritte",

View File

@@ -26,6 +26,7 @@
"timeout": "Location request timed out."
},
"add": "Add Profile",
"save": "Save Profile",
"edit": "Edit",
"delete": "Delete",
"noItems": "No profiles yet. Create one to get started."
@@ -62,6 +63,7 @@
"tooltip": "Current or target indoor temperature. Default 25\u00b0C."
},
"add": "Add Room",
"save": "Save Room",
"noItems": "No rooms yet. Add rooms to your profile."
},
"devices": {
@@ -75,6 +77,7 @@
"wattsPeak": { "label": "Watts (Peak)", "tooltip": "Power draw at maximum load (e.g. gaming)" },
"dutyCycle": { "label": "Duty Cycle", "tooltip": "Fraction of time active (0\u20131). Fridge \u2248 0.3, PC \u2248 1.0." },
"add": "Add Device",
"save": "Save Device",
"noItems": "No devices yet."
},
"occupants": {
@@ -89,6 +92,7 @@
},
"vulnerable": { "label": "Vulnerable", "tooltip": "Check if elderly, young children, or health-compromised. Adds care reminders." },
"add": "Add Occupants",
"save": "Save Occupants",
"noItems": "No occupants yet."
},
"ac": {
@@ -105,6 +109,7 @@
"dehumidify": { "label": "Dehumidify", "tooltip": "Whether the unit has a dehumidify mode" },
"rooms": { "label": "Assigned Rooms", "tooltip": "Which rooms this AC unit serves" },
"add": "Add AC Unit",
"save": "Save AC Unit",
"noItems": "No AC units yet."
},
"toggles": {
@@ -173,7 +178,23 @@
"riskLow": "Low",
"riskModerate": "Moderate",
"riskHigh": "High",
"riskExtreme": "Extreme"
"riskExtreme": "Extreme",
"noActions": "No actions",
"effort": "Effort",
"impact": "Impact",
"aiDisclaimer": "AI-generated summary. Not a substitute for professional advice.",
"coolVentilate": "Open windows",
"coolAC": "AC cooling",
"coolOverloaded": "AC overloaded",
"aiActions": "AI-recommended actions",
"category": {
"shading": "Shading",
"ventilation": "Ventilation",
"internal_gains": "Heat Sources",
"ac_strategy": "AC Strategy",
"hydration": "Hydration",
"care": "Care"
}
},
"guide": {
"title": "Getting Started",

View File

@@ -2,6 +2,10 @@
(function() {
"use strict";
// SVG icon templates
const iconEdit = '<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 3a2.83 2.83 0 114 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>';
const iconDelete = '<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2"/></svg>';
// Tab switching
const tabBtns = document.querySelectorAll(".tab-btn");
const tabPanels = document.querySelectorAll(".tab-panel");
@@ -67,6 +71,7 @@
form.reset();
const hidden = form.querySelector('input[name="id"]');
if (hidden) hidden.value = "";
exitEditMode(form);
}
function numOrDefault(val, def) {
@@ -74,6 +79,47 @@
return isNaN(n) ? def : n;
}
// Edit mode helpers
function enterEditMode(form) {
form.classList.add("ring-2", "ring-orange-400");
const submitBtn = form.querySelector(".submit-btn");
const cancelBtn = form.querySelector(".cancel-btn");
if (submitBtn) submitBtn.textContent = submitBtn.dataset.saveText;
if (cancelBtn) cancelBtn.classList.remove("hidden");
}
function exitEditMode(form) {
form.classList.remove("ring-2", "ring-orange-400");
const submitBtn = form.querySelector(".submit-btn");
const cancelBtn = form.querySelector(".cancel-btn");
if (submitBtn) submitBtn.textContent = submitBtn.dataset.addText;
if (cancelBtn) cancelBtn.classList.add("hidden");
}
// Wire up all cancel buttons
document.querySelectorAll(".cancel-btn").forEach(btn => {
btn.addEventListener("click", () => {
const form = btn.closest("form");
if (form) resetForm(form);
});
});
// List item card classes
const cardClasses = "bg-white dark:bg-gray-800 rounded-lg p-3 shadow-sm flex items-center justify-between hover:shadow-md transition-all duration-200";
// Action button builders
function editBtn(onclick) {
return `<button onclick="${onclick}" class="p-1.5 rounded-lg text-gray-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/30 transition" title="Edit">${iconEdit}</button>`;
}
function deleteBtn(onclick) {
return `<button onclick="${onclick}" class="p-1.5 rounded-lg text-gray-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 transition" title="Delete">${iconDelete}</button>`;
}
function actionGroup(editOnclick, deleteOnclick) {
return `<div class="flex items-center gap-1">${editBtn(editOnclick)}${deleteBtn(deleteOnclick)}</div>`;
}
// ========== Profiles ==========
async function loadProfiles() {
const profiles = await dbGetAll("profiles");
@@ -86,17 +132,16 @@
list.innerHTML = profiles.map(p => {
const isActive = activeId === p.id;
return `
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 shadow-sm flex items-center justify-between">
<div class="${cardClasses}">
<div>
<span class="font-medium">${esc(p.name)}</span>
<span class="text-xs text-gray-400 ml-2">${p.latitude.toFixed(4)}, ${p.longitude.toFixed(4)} · ${esc(p.timezone || "")}</span>
</div>
<div class="flex gap-2">
<div class="flex items-center gap-2">
<button onclick="activateProfile(${p.id})" class="text-xs px-2 py-1 rounded ${isActive ? 'bg-orange-600 text-white' : 'bg-gray-100 dark:bg-gray-700'}">
${isActive ? '● Active' : 'Set Active'}
</button>
<button onclick="editProfileUI(${p.id})" class="text-xs text-blue-500 hover:text-blue-700">✎</button>
<button onclick="deleteProfileUI(${p.id})" class="text-xs text-red-500 hover:text-red-700">✕</button>
${actionGroup(`editProfileUI(${p.id})`, `deleteProfileUI(${p.id})`)}
</div>
</div>`;
}).join("");
@@ -118,6 +163,7 @@
form.querySelector('input[name="latitude"]').value = p.latitude;
form.querySelector('input[name="longitude"]').value = p.longitude;
form.querySelector('input[name="timezone"]').value = p.timezone || "Europe/Berlin";
enterEditMode(form);
form.querySelector('input[name="name"]').focus();
};
@@ -188,13 +234,12 @@
return;
}
list.innerHTML = rooms.map(r => `
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 shadow-sm flex items-center justify-between">
<div class="${cardClasses}">
<div>
<span class="font-medium">${esc(r.name)}</span>
<span class="text-xs text-gray-400 ml-2">${r.areaSqm}m² · ${r.orientation} · SHGC ${r.shgc} · ${r.indoorTempC || 25}°C</span>
</div>
<button onclick="editRoomUI(${r.id})" class="text-xs text-blue-500 hover:text-blue-700">✎</button>
<button onclick="deleteRoomUI(${r.id})" class="text-xs text-red-500 hover:text-red-700">✕</button>
${actionGroup(`editRoomUI(${r.id})`, `deleteRoomUI(${r.id})`)}
</div>
`).join("");
}
@@ -216,6 +261,7 @@
form.querySelector('input[name="shgc"]').value = r.shgc || 0.6;
form.querySelector('select[name="insulation"]').value = r.insulation || "average";
form.querySelector('input[name="indoorTempC"]').value = r.indoorTempC || 25;
enterEditMode(form);
form.querySelector('input[name="name"]').focus();
};
@@ -276,13 +322,12 @@
return;
}
list.innerHTML = allDevices.map(d => `
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 shadow-sm flex items-center justify-between">
<div class="${cardClasses}">
<div>
<span class="font-medium">${esc(d.name)}</span>
<span class="text-xs text-gray-400 ml-2">${esc(d._roomName)} · ${d.wattsTypical}W typical</span>
</div>
<button onclick="editDeviceUI(${d.id})" class="text-xs text-blue-500 hover:text-blue-700">✎</button>
<button onclick="deleteDeviceUI(${d.id})" class="text-xs text-red-500 hover:text-red-700">✕</button>
${actionGroup(`editDeviceUI(${d.id})`, `deleteDeviceUI(${d.id})`)}
</div>
`).join("");
}
@@ -299,6 +344,7 @@
form.querySelector('input[name="wattsTypical"]').value = d.wattsTypical || 0;
form.querySelector('input[name="wattsPeak"]').value = d.wattsPeak || 0;
form.querySelector('input[name="dutyCycle"]').value = d.dutyCycle ?? 1.0;
enterEditMode(form);
form.querySelector('input[name="name"]').focus();
};
@@ -347,13 +393,12 @@
return;
}
list.innerHTML = allOccupants.map(o => `
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 shadow-sm flex items-center justify-between">
<div class="${cardClasses}">
<div>
<span class="font-medium">${o.count}x ${esc(o.activityLevel)}</span>
<span class="text-xs text-gray-400 ml-2">${esc(o._roomName)}${o.vulnerable ? ' · ⚠ vulnerable' : ''}</span>
</div>
<button onclick="editOccupantUI(${o.id})" class="text-xs text-blue-500 hover:text-blue-700">✎</button>
<button onclick="deleteOccupantUI(${o.id})" class="text-xs text-red-500 hover:text-red-700">✕</button>
${actionGroup(`editOccupantUI(${o.id})`, `deleteOccupantUI(${o.id})`)}
</div>
`).join("");
}
@@ -367,6 +412,7 @@
form.querySelector('input[name="count"]').value = o.count || 1;
form.querySelector('select[name="activityLevel"]').value = o.activityLevel || "sedentary";
form.querySelector('input[name="vulnerable"]').checked = !!o.vulnerable;
enterEditMode(form);
form.querySelector('input[name="count"]').focus();
};
@@ -414,13 +460,12 @@
const roomIds = assignments.filter(a => a.acId === u.id).map(a => a.roomId);
const roomNames = roomIds.map(id => roomMap[id] || `Room ${id}`).join(", ");
return `
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 shadow-sm flex items-center justify-between">
<div class="${cardClasses}">
<div>
<span class="font-medium">${esc(u.name)}</span>
<span class="text-xs text-gray-400 ml-2">${u.capacityBtu} BTU · ${esc(u.acType)}${roomNames ? ' · ' + esc(roomNames) : ''}</span>
</div>
<button onclick="editACUI(${u.id})" class="text-xs text-blue-500 hover:text-blue-700">✎</button>
<button onclick="deleteACUI(${u.id})" class="text-xs text-red-500 hover:text-red-700">✕</button>
${actionGroup(`editACUI(${u.id})`, `deleteACUI(${u.id})`)}
</div>
`;
}).join("");
@@ -442,6 +487,7 @@
document.querySelectorAll('#ac-room-checkboxes input').forEach(cb => {
cb.checked = assignedRoomIds.has(parseInt(cb.value));
});
enterEditMode(form);
form.querySelector('input[name="name"]').focus();
};

View File

@@ -32,7 +32,7 @@
</div>
</nav>
<main class="mx-auto px-4 sm:px-6 lg:px-8 py-6">
<main class="max-w-[1800px] mx-auto px-4 sm:px-6 lg:px-8 py-6">
{{block "content" .}}{{end}}
</main>

View File

@@ -43,7 +43,8 @@
<button type="button" id="geolocate-btn" class="px-3 py-1.5 text-sm bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition">
📍 {{t "setup.profiles.geolocate.button"}}
</button>
<button type="submit" class="px-4 py-1.5 text-sm bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition">{{t "setup.profiles.add"}}</button>
<button type="submit" class="submit-btn px-4 py-1.5 text-sm bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition" data-add-text="{{t "setup.profiles.add"}}" data-save-text="{{t "setup.profiles.save"}}">{{t "setup.profiles.add"}}</button>
<button type="button" class="cancel-btn hidden px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 transition">{{t "common.cancel"}}</button>
</div>
<input type="hidden" name="id" value="">
</form>
@@ -117,7 +118,8 @@
</div>
</div>
<div class="flex gap-2">
<button type="submit" class="px-4 py-1.5 text-sm bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition">{{t "setup.rooms.add"}}</button>
<button type="submit" class="submit-btn px-4 py-1.5 text-sm bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition" data-add-text="{{t "setup.rooms.add"}}" data-save-text="{{t "setup.rooms.save"}}">{{t "setup.rooms.add"}}</button>
<button type="button" class="cancel-btn hidden px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 transition">{{t "common.cancel"}}</button>
</div>
<input type="hidden" name="id" value="">
<input type="hidden" name="profileId" value="">
@@ -161,7 +163,10 @@
<input type="number" name="dutyCycle" step="0.1" min="0" max="1" value="1.0" class="w-full px-3 py-1.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-sm">
</div>
</div>
<button type="submit" class="px-4 py-1.5 text-sm bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition">{{t "setup.devices.add"}}</button>
<div class="flex gap-2">
<button type="submit" class="submit-btn px-4 py-1.5 text-sm bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition" data-add-text="{{t "setup.devices.add"}}" data-save-text="{{t "setup.devices.save"}}">{{t "setup.devices.add"}}</button>
<button type="button" class="cancel-btn hidden px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 transition">{{t "common.cancel"}}</button>
</div>
<input type="hidden" name="id" value="">
</form>
<div id="devices-list" class="space-y-2">
@@ -196,7 +201,10 @@
</label>
</div>
</div>
<button type="submit" class="px-4 py-1.5 text-sm bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition">{{t "setup.occupants.add"}}</button>
<div class="flex gap-2">
<button type="submit" class="submit-btn px-4 py-1.5 text-sm bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition" data-add-text="{{t "setup.occupants.add"}}" data-save-text="{{t "setup.occupants.save"}}">{{t "setup.occupants.add"}}</button>
<button type="button" class="cancel-btn hidden px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 transition">{{t "common.cancel"}}</button>
</div>
<input type="hidden" name="id" value="">
</form>
<div id="occupants-list" class="space-y-2">
@@ -239,7 +247,10 @@
<label class="block text-sm font-medium mb-1">{{t "setup.ac.rooms.label"}} <span class="tooltip-trigger" data-tooltip="{{t "setup.ac.rooms.tooltip"}}">?</span></label>
<div id="ac-room-checkboxes" class="flex flex-wrap gap-2"></div>
</div>
<button type="submit" class="px-4 py-1.5 text-sm bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition">{{t "setup.ac.add"}}</button>
<div class="flex gap-2">
<button type="submit" class="submit-btn px-4 py-1.5 text-sm bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition" data-add-text="{{t "setup.ac.add"}}" data-save-text="{{t "setup.ac.save"}}">{{t "setup.ac.add"}}</button>
<button type="button" class="cancel-btn hidden px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 transition">{{t "common.cancel"}}</button>
</div>
<input type="hidden" name="id" value="">
</form>
<div id="ac-list" class="space-y-2">