From f6e04148667bf4c255bc4488f4865a9b5b631bbe Mon Sep 17 00:00:00 2001
From: Moirtz Wagner
Date: Mon, 31 Aug 2026 21:38:09 +0200
Subject: [PATCH] Raum-Modal: Gruppen als Tabs, Jalousien senkrecht bedienen
Die Gruppenueberschriften sind Tabs geworden, Beschattung zuerst - das ist
es, was man in einem Raum am haeufigsten anfasst. Vorher scrollte man an
drei Rollladen vorbei, um zum Licht zu kommen. Bei nur einer Gruppe bleibt
die Leiste weg; auf schmalen Schirmen scrollt sie seitwaerts, statt die
Namen umzubrechen.
Bei den Jalousien stehen Auf, Stop und Zu jetzt untereinander - so zeigen
die Knoepfe in die Richtung, in die sie fahren. Die Hoehe daneben ist ein
senkrechter Regler mit "oben" und "unten" an den Enden: der Griff steht
dort, wo der Rollladen steht, und niemand muss "100 %" uebersetzen.
Die Neigung hat ein Bild dazubekommen. "30 %" oder "70 %" sagt nicht, ob
man noch hinaussieht; vier Streifen sagen es. Gezeichnet wird die Hoehe,
die eine gekippte Lamelle von vorn zeigt (w * sin(Winkel)) - bei "auf"
sieht man nur ihre Kante und viel Luft dazwischen, bei "zu" deckt sie den
Abstand zur naechsten vollstaendig ab. Genau das, was das Licht abhaelt.
Die Regler faerben ihre Bahn jetzt mit, wie es der Temperaturregler seit
jeher tut - dieselbe Eigenschaft --background-size, nur beim senkrechten
von oben nach unten.
css/solar.css hatte gemischte Zeilenenden und ist dabei durchgehend auf LF
gebracht worden; das erklaert den grossen Diff bei kleiner Aenderung.
Co-Authored-By: Claude Opus 5
---
ajax/room.php | 14 +-
css/solar.css | 285 +++++++++++++++++++++++-------------
js/solar/homeMQTT.js | 59 +++++++-
restricted/roomControls.php | 188 +++++++++++++++++++++---
4 files changed, 403 insertions(+), 143 deletions(-)
diff --git a/ajax/room.php b/ajax/room.php
index 518d77c..beb5f95 100644
--- a/ajax/room.php
+++ b/ajax/room.php
@@ -133,19 +133,7 @@ if (!$gruppen && !$hatThermostat) {
. "Das geht über das Haus-Symbol in der Karte „Automatismen“.
";
}
-foreach (raumGruppenTitel() as $form => $titel) {
- if (empty($gruppen[$form])) {
- continue;
- }
- // Eine einzelne Lampe braucht keine Ueberschrift "Licht" - der Name der
- // Lampe steht direkt darunter. Ueberschriften ordnen erst ab zwei.
- if (count($gruppen[$form]) > 1) {
- echo gruppenTitel($titel[0], $titel[1]);
- }
- foreach ($gruppen[$form] as $d) {
- echo zeichneGeraet($d, $form);
- }
-}
+echo zeichneGruppen($gruppen);
echo "";
echo "";
diff --git a/css/solar.css b/css/solar.css
index 1bbac26..577b901 100644
--- a/css/solar.css
+++ b/css/solar.css
@@ -1,15 +1,15 @@
-.winddir {
- position:absolute;
- left:60%;
- width:25%;
- height:40%;
- padding: 0;
- background-image: url('../assets/img/arrow.svg');
- background-size:contain;
- background-repeat: no-repeat;
- background-position: center;
- }
-
+.winddir {
+ position:absolute;
+ left:60%;
+ width:25%;
+ height:40%;
+ padding: 0;
+ background-image: url('../assets/img/arrow.svg');
+ background-size:contain;
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+
/* Schieberegler in den Modals (Autoladung, Heizstab, Thermostat).
Der gefuellte Anteil kommt aus --background-size, gesetzt von
@@ -111,91 +111,176 @@ input[type='range']::-webkit-slider-runnable-track {
.input-group.raum-cmd > .form-select {
min-width: 5rem;
}
-
-
-/* --------------------------------------------------------------------------
- Raum-Modal: Bedienelemente
- Erzeugt von restricted/roomControls.php, verdrahtet in js/solar/homeMQTT.js.
- -------------------------------------------------------------------------- */
-
-/* Ueberschrift einer Geraetegruppe (Beschattung, Licht, ...). Klein und
- ruhig - sie ordnet, sie soll nicht mit den Geraeten um Aufmerksamkeit
- ringen. */
-.raum-gruppe {
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- margin: 0.75rem 0 0.35rem;
-}
-.raum-gruppe:first-child {
- margin-top: 0;
-}
-
-/* Waehrend ein Kommando unterwegs ist. Ein "Zu" an einer Jalousie dauert
- ueber eine Minute; ohne sichtbare Sperre tippt man in der Zeit nach. */
-.raum-geraet.raum-warte {
- opacity: 0.55;
- pointer-events: none;
-}
-.raum-geraet.raum-warte::after {
- content: "";
- position: absolute;
- top: 0.5rem;
- right: 0.5rem;
- width: 0.9rem;
- height: 0.9rem;
- border: 2px solid currentColor;
- border-right-color: transparent;
- border-radius: 50%;
- animation: raum-dreht 0.8s linear infinite;
-}
-.raum-geraet {
- position: relative;
-}
-@keyframes raum-dreht {
- to { transform: rotate(360deg); }
-}
-
-/* Schieberegler stehen dicht untereinander (Position und Neigung). Ohne den
- Abstand kleben Beschriftung und Bahn aneinander. */
-.raum-regler + .raum-regler {
- margin-top: 0.35rem;
-}
-.raum-regler .form-range {
- margin-bottom: 0;
-}
-
-/* Messwerte als Gitter statt als Textwurst: ein Stromzaehler hat sechzehn
- Werte, die in einer Zeile niemand findet. auto-fill statt fester Spalten,
- damit auf dem Handy zwei und auf dem Schirm vier nebeneinander passen. */
-.raum-werte {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
- gap: 0.4rem;
-}
-.raum-wert {
- background: rgba(128, 128, 128, 0.08);
- border-radius: 0.35rem;
- padding: 0.3rem 0.45rem;
- overflow: hidden;
-}
-.raum-wert-zahl {
- font-size: 0.95rem;
- font-weight: 600;
- line-height: 1.2;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.raum-wert-einheit {
- font-size: 0.7rem;
- font-weight: 400;
- opacity: 0.7;
-}
-.raum-wert-name {
- font-size: 0.68rem;
- opacity: 0.7;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
+
+
+/* --------------------------------------------------------------------------
+ Raum-Modal: Bedienelemente
+ Erzeugt von restricted/roomControls.php, verdrahtet in js/solar/homeMQTT.js.
+ -------------------------------------------------------------------------- */
+
+/* Die Geraetegruppen als Tabs. Auf einem Handy passen fuenf Beschriftungen
+ nicht nebeneinander - dann scrollt die Leiste seitwaerts, statt die Namen
+ umzubrechen. */
+.raum-tabs {
+ overflow-x: auto;
+ overflow-y: hidden;
+ scrollbar-width: none;
+}
+.raum-tabs::-webkit-scrollbar {
+ display: none;
+}
+.raum-tabs .nav-link {
+ padding: 0.25rem 0.6rem;
+ font-size: 0.8rem;
+ white-space: nowrap;
+}
+
+/* Waehrend ein Kommando unterwegs ist. Ein "Zu" an einer Jalousie dauert
+ ueber eine Minute; ohne sichtbare Sperre tippt man in der Zeit nach. */
+.raum-geraet.raum-warte {
+ opacity: 0.55;
+ pointer-events: none;
+}
+.raum-geraet.raum-warte::after {
+ content: "";
+ position: absolute;
+ top: 0.5rem;
+ right: 0.5rem;
+ width: 0.9rem;
+ height: 0.9rem;
+ border: 2px solid currentColor;
+ border-right-color: transparent;
+ border-radius: 50%;
+ animation: raum-dreht 0.8s linear infinite;
+}
+.raum-geraet {
+ position: relative;
+}
+@keyframes raum-dreht {
+ to { transform: rotate(360deg); }
+}
+
+/* Schieberegler stehen dicht untereinander (Position und Neigung). Ohne den
+ Abstand kleben Beschriftung und Bahn aneinander. */
+.raum-regler + .raum-regler {
+ margin-top: 0.35rem;
+}
+.raum-regler .form-range {
+ margin-bottom: 0;
+}
+
+/* Messwerte als Gitter statt als Textwurst: ein Stromzaehler hat sechzehn
+ Werte, die in einer Zeile niemand findet. auto-fill statt fester Spalten,
+ damit auf dem Handy zwei und auf dem Schirm vier nebeneinander passen. */
+.raum-werte {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
+ gap: 0.4rem;
+}
+.raum-wert {
+ background: rgba(128, 128, 128, 0.08);
+ border-radius: 0.35rem;
+ padding: 0.3rem 0.45rem;
+ overflow: hidden;
+}
+.raum-wert-zahl {
+ font-size: 0.95rem;
+ font-weight: 600;
+ line-height: 1.2;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.raum-wert-einheit {
+ font-size: 0.7rem;
+ font-weight: 400;
+ opacity: 0.6;
+}
+.raum-wert-name {
+ font-size: 0.68rem;
+ opacity: 0.6;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+
+/* --------------------------------------------------------------------------
+ Raum-Modal: Jalousien
+ -------------------------------------------------------------------------- */
+
+/* Fahrknoepfe, Hoehe und Neigung nebeneinander. Die Neigung nimmt den Rest -
+ sie ist die einzige der drei Spalten, die von Breite profitiert. */
+.raum-jalousie {
+ display: flex;
+ align-items: stretch;
+ gap: 0.6rem;
+}
+.raum-jalousie .raum-neigung-block {
+ flex: 1 1 auto;
+ min-width: 0;
+ align-self: center;
+}
+.raum-jalousie .dropdown {
+ align-self: flex-start;
+}
+
+/* Auf, Stop, Zu untereinander - die Knoepfe zeigen in die Richtung, in die
+ sie fahren. Schmal, weil nur Symbole darin stehen. */
+.raum-fahrt {
+ flex: 0 0 auto;
+}
+.raum-fahrt .btn {
+ padding: 0.2rem 0.55rem;
+ line-height: 1.4;
+}
+
+/* Die Hoehe als senkrechter Regler: der Griff steht dort, wo der Rollladen
+ steht. writing-mode ist der Weg, den Chrome und Firefox heute gehen -
+ slider-vertical war nie standardisiert und ist inzwischen entfernt. */
+.raum-position {
+ flex: 0 0 auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.1rem;
+}
+.raum-position input[type='range'] {
+ writing-mode: vertical-rl;
+ width: 1.2rem;
+ height: 4.5rem;
+ padding: 0;
+ margin: 0;
+}
+/* Die gefuellte Bahn laeuft hier von oben nach unten mit - "voll" heisst
+ heruntergefahren. */
+.raum-position input[type='range']::-webkit-slider-runnable-track {
+ background: linear-gradient(to bottom, #00788F, #00788F), #D7D7D7;
+ background-size: 100% var(--background-size, 0%);
+}
+
+/* Die Endstellungen in Worten statt in Prozent. */
+.raum-ende {
+ font-size: 0.68rem;
+ opacity: 0.65;
+ line-height: 1.1;
+}
+
+/* Die Lamellen zur Neigung. Ihre Hoehe setzt lamellenZeichnen() in
+ js/solar/homeMQTT.js - schmal mit Luft dazwischen heisst offen,
+ aneinanderliegend heisst geschlossen. */
+.raum-lamellen {
+ display: block;
+ width: 100%;
+ height: 1.8rem;
+ margin: 0.1rem 0;
+ /* Der Rahmen macht aus dem Streifen ein Fenster - ohne ihn liest sich das
+ geschlossene Bild wie eine zweite Reglerbahn. */
+ background: rgba(0, 120, 143, 0.14);
+ border: 1px solid rgba(128, 128, 128, 0.35);
+ border-radius: 0.2rem;
+}
+.raum-lamelle {
+ fill: currentColor;
+ opacity: 0.6;
+}
diff --git a/js/solar/homeMQTT.js b/js/solar/homeMQTT.js
index 915dd79..e95c463 100644
--- a/js/solar/homeMQTT.js
+++ b/js/solar/homeMQTT.js
@@ -226,14 +226,13 @@ function switchFloor(floor){
k.addEventListener("click", () => schalte(k));
});
- // Schieberegler: waehrend des Ziehens nur die Beschriftung, geschickt
- // wird erst beim Loslassen. Sonst bekaeme eine Jalousie bei jedem Pixel
- // ein neues Ziel und kaeme aus dem Anfahren nicht mehr heraus.
+ // Schieberegler: waehrend des Ziehens nur die Anzeige, geschickt wird
+ // erst beim Loslassen. Sonst bekaeme eine Jalousie bei jedem Pixel ein
+ // neues Ziel und kaeme aus dem Anfahren nicht mehr heraus.
koerper.querySelectorAll(".raum-regler").forEach(regler => {
const schieber = regler.querySelector("input[type=range]");
- const anzeige = regler.querySelector(".raum-regler-wert");
- const einheit = schieber.dataset.einheit || "";
- schieber.addEventListener("input", () => { anzeige.textContent = schieber.value + einheit; });
+ reglerAnzeigen(regler);
+ schieber.addEventListener("input", () => reglerAnzeigen(regler));
schieber.addEventListener("change", () => {
const params = {};
params[regler.dataset.param] = schieber.value;
@@ -280,6 +279,54 @@ function switchFloor(floor){
});
}
+ /**
+ * Anzeige, gefuellte Bahn und Lamellenbild eines Reglers nachziehen.
+ *
+ * An den Endstellungen steht ein Wort statt der Zahl: "unten" liest sich
+ * ohne Umweg, "100 %" muss man erst uebersetzen. Dazwischen bleibt es bei
+ * der Prozentzahl - fuer halb heruntergefahren gibt es kein Wort.
+ */
+ function reglerAnzeigen(regler) {
+ const schieber = regler.querySelector("input[type=range]");
+ const anzeige = regler.querySelector(".raum-regler-wert");
+ const wert = Number(schieber.value);
+
+ if (anzeige) {
+ const worte = regler.dataset.worte ? JSON.parse(regler.dataset.worte) : null;
+ anzeige.textContent = (worte && worte[wert] !== undefined)
+ ? worte[wert]
+ : wert + (schieber.dataset.einheit || "");
+ }
+
+ // Der gefuellte Anteil der Bahn - dieselbe Eigenschaft, aus der auch
+ // bindModalSlider() in common.js den Temperaturregler faerbt.
+ const min = Number(schieber.min);
+ const max = Number(schieber.max);
+ schieber.style.setProperty("--background-size",
+ ((wert - min) * 100 / (max - min)) + "%");
+
+ lamellenZeichnen(regler.closest(".raum-neigung-block"), wert, min, max);
+ }
+
+ /**
+ * Die Lamellen zur Neigung zeichnen.
+ *
+ * Gezeichnet wird die Hoehe, die eine gekippte Lamelle von vorn zeigt:
+ * w * sin(Winkel). Bei 0 % steht sie waagerecht und man sieht nur ihre
+ * Kante, bei 100 % steht sie quer und deckt den Abstand zur naechsten
+ * vollstaendig ab - genau das, was das Licht abhaelt.
+ */
+ function lamellenZeichnen(block, wert, min, max) {
+ if (!block) return;
+ const anteil = (wert - min) / (max - min);
+ const hoehe = Math.max(1.6, 11 * Math.sin(anteil * Math.PI / 2));
+ block.querySelectorAll(".raum-lamelle").forEach(lamelle => {
+ const mitte = Number(lamelle.dataset.mitte);
+ lamelle.setAttribute("height", hoehe);
+ lamelle.setAttribute("y", mitte - hoehe / 2);
+ });
+ }
+
/**
* Die Ist-Werte im Heizungsteil aus dem laufenden MQTT-Strom fuellen.
*
diff --git a/restricted/roomControls.php b/restricted/roomControls.php
index 10f0907..cdd4b64 100644
--- a/restricted/roomControls.php
+++ b/restricted/roomControls.php
@@ -137,18 +137,72 @@ function bedienform($d)
return "sonstiges";
}
-/** Ueberschriften der Gruppen, in der Reihenfolge, in der sie erscheinen. */
+/**
+ * Die Gruppen in der Reihenfolge, in der ihre Tabs erscheinen.
+ *
+ * Beschattung zuerst: das ist es, was man in einem Raum am haeufigsten
+ * anfasst. Messwerte zuletzt - die schaut man an, man bedient sie nicht.
+ */
function raumGruppenTitel()
{
return [
"beschattung" => ["Beschattung", "bi-blinds"],
"licht" => ["Licht", "bi-lightbulb"],
"schalter" => ["Schalten", "bi-toggle-on"],
- "sonstiges" => ["Weitere Geräte", "bi-sliders"],
+ "sonstiges" => ["Geräte", "bi-sliders"],
"messwerte" => ["Messwerte", "bi-speedometer2"],
];
}
+/**
+ * Die Geraetegruppen als Tabs.
+ *
+ * Vorher standen sie untereinander, und man scrollte an drei Rollladen
+ * vorbei, um zum Licht zu kommen. Bei nur einer Gruppe bleibt die Leiste
+ * weg - ein einzelner Tab beschriftet nur sich selbst.
+ */
+function zeichneGruppen($gruppen)
+{
+ $vorhanden = [];
+ foreach (raumGruppenTitel() as $form => $titel) {
+ if (!empty($gruppen[$form])) {
+ $vorhanden[$form] = $titel;
+ }
+ }
+ if (!$vorhanden) {
+ return "";
+ }
+
+ $html = "";
+ if (count($vorhanden) > 1) {
+ $html .= "
";
+}
+
+/**
+ * Die Lamellenstellung mit einem Bild daneben.
+ *
+ * Bei 30 % oder 70 % sagt die Zahl nicht, ob man noch hinaussieht. Die vier
+ * Streifen zeigen es: schmal mit Luft dazwischen heisst offen, geschlossen
+ * liegen sie aneinander. Gezeichnet wird die Hoehe, die eine gekippte
+ * Lamelle von vorn zeigt - genau das, was das Licht abhaelt.
+ */
+function neigungsRegler($cmd, $param, $wert)
+{
+ $min = reglerMin($param);
+ $max = reglerMax($param);
+ $wert = reglerWert($wert, $min, $max);
+ return "