Kachelsymbole: jedes Bootstrap-Icon statt einer Handvoll

Fuer die Symbole vor den Zahlen standen drei Codepunkte von Hand in
rooms.php, und die Maske bot genau diese drei an. Jedes weitere Symbol waere
eine Codeaenderung gewesen.

Die Codepunkte stehen aber schon im Haus: css/bootstrap-icons.min.css fuehrt
sie unter .bi-<name>::before - dieselbe Quelle, aus der auch der Browser
liest. bootstrapIcons() liest sie von dort (2078 Stueck, einmal je Anfrage),
kachelIcon() schlaegt darin nach. Damit ist jedes Symbol moeglich, und ein
Versionswechsel der Icons zieht von selbst mit.

In der Maske steht deshalb kein Auswahlfeld mehr, sondern der Name mit einer
Vorschau davor - "moisture" sagt nichts, das Symbol daneben schon. Die
Vorschlagsliste beginnt mit gut dreissig ueblichen Symbolen unter deutscher
Beschriftung (Temperatur, Feuchte, Leistung, Wallbox, Regen ...), danach folgt
der ganze Rest alphabetisch. Ein Name, den es nicht gibt, zeigt sofort ein
rotes Fragezeichen und wird beim Speichern mit Begruendung abgelehnt.

In der eingeklappten Kurzfassung einer Kachel steht das Symbol jetzt mit -
es ist das schnellste Erkennungszeichen dafuer, welcher der drei Werte
gemeint ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 23:24:14 +02:00
co-authored by Claude Opus 5
parent eb1aa8c760
commit cd398bc800
5 changed files with 234 additions and 31 deletions
+2 -1
View File
@@ -100,7 +100,8 @@ if ($action === "kacheln") {
try {
jsonAntwort([
"kacheln" => kachelListe(),
"symbole" => kachelSymbole(),
// Alle Symbolnamen - die üblichen zuerst und mit Beschriftung.
"symbole" => kachelIconNamen(),
// Auswahlliste je Geraet - dieselbe Quelle wie im Automatik-Editor.
"katalog" => kachelKatalog(),
// Fuer die Eingabe von Hand, wenn etwas nicht im Katalog steht.
+20 -1
View File
@@ -1927,6 +1927,7 @@ input[type='range']::-webkit-slider-runnable-track {
white-space: nowrap;
}
.kachel-chip.leer { font-style: italic; }
.kachel-chip .bi { margin-right: .3rem; opacity: .8; }
.kachel-editor {
padding: .25rem .75rem .75rem;
@@ -1937,13 +1938,31 @@ input[type='range']::-webkit-slider-runnable-track {
untereinander - die Felder sind zu kurz, um sie zu quetschen. */
.kachel-wert {
display: grid;
grid-template-columns: 7.5rem minmax(10rem, 1fr) 7rem 5.5rem 4rem auto;
grid-template-columns: 7.5rem minmax(10rem, 1fr) 7rem 5.5rem 9.5rem auto;
gap: .4rem;
align-items: center;
padding: .5rem 0;
border-bottom: 1px dashed var(--ton-linie, rgba(138, 148, 158, .2));
}
.kachel-wert:last-of-type { border-bottom: 0; }
/* Symbolname mit Vorschau davor: die Vorschau sagt, was der Name bedeutet -
"moisture" allein sagt es nicht. */
.kachel-wert .kw-symbol {
display: flex;
align-items: center;
gap: .35rem;
}
.kachel-wert .kw-symbol-schau {
flex: none;
width: 1.5rem;
text-align: center;
font-size: 1.05rem;
line-height: 1;
color: var(--ton-schrift-2, #9aa6ad);
}
.kachel-wert .kw-symbol-schau .unbekannt { color: var(--ton-schlecht, #e06060); }
.kachel-wert .kw-symbol input { min-width: 0; }
.kachel-wert .kw-schalter {
display: flex;
align-items: center;
+75 -10
View File
@@ -376,9 +376,14 @@ function zeichneKacheln() {
/** Eine Raumzeile: Name, Kurzfassung der Werte, darunter der Editor. */
function kachelZeile(k) {
const werte = k._entwurf || k.werte;
// Das Symbol steht auch in der Kurzfassung: es ist das, was auf der Kachel
// vor der Zahl steht, und damit der schnellste Weg zu erkennen, welcher der
// drei Werte gemeint ist.
const kurz = werte.length
? werte.map(w => '<span class="kachel-chip">' + textSchuetzen(kachelWertKurz(w, k))
+ "</span>").join("")
? werte.map(w => '<span class="kachel-chip">'
+ (w.icon && kachelSymbolBekannt(w.icon)
? '<i class="bi bi-' + textSchuetzen(w.icon) + '"></i>' : "")
+ textSchuetzen(kachelWertKurz(w, k)) + "</span>").join("")
: '<span class="kachel-chip leer">ohne Messwert</span>';
let html = '<div class="kachel-zeile' + (k._offen ? " offen" : "") + '"'
@@ -407,6 +412,59 @@ function kachelZeile(k) {
return html + "</div>";
}
/* --- Symbole ------------------------------------------------------------- */
/**
* Das Feld für das Symbol: eine Vorschau und ein Name.
*
* Statt einer Auswahlliste mit einer Handvoll Symbole steht hier der Name des
* Bootstrap-Icons - damit sind alle gut zweitausend möglich, ohne dass für
* jedes neue eines im Code nachgetragen werden müsste. Die üblichen stehen in
* der Vorschlagsliste obenan, mit deutscher Beschriftung; wer ein anderes
* will, tippt und bekommt den Rest angeboten (icons.getbootstrap.com).
*
* Die Vorschau ist die eigentliche Hilfe: ein Name wie "moisture" sagt nichts,
* das Symbol daneben schon.
*/
function kachelSymbolFeld(icon) {
return '<span class="kw-symbol-schau">' + kachelSymbolVorschau(icon) + "</span>"
+ '<input type="text" class="form-control form-control-sm" data-feld="icon"'
+ ' list="kachelIcons" placeholder="Symbol" autocomplete="off"'
+ ' value="' + textSchuetzen(icon) + '">';
}
/** Der Inhalt der Vorschau: das Symbol, ein Strich, oder ein Fragezeichen. */
function kachelSymbolVorschau(icon) {
if (!icon) return '<i class="bi bi-dash" title="ohne Symbol"></i>';
if (!kachelSymbolBekannt(icon)) {
return '<i class="bi bi-question-circle unbekannt" title="Dieses Symbol gibt es nicht"></i>';
}
return '<i class="bi bi-' + textSchuetzen(icon) + '" title="' + textSchuetzen(icon) + '"></i>';
}
function kachelSymbolBekannt(icon) {
if (!kachelSymbolBekannt._satz) {
kachelSymbolBekannt._satz = new Set((kachelModell.symbole || []).map(s => s.name));
}
return kachelSymbolBekannt._satz.has(icon);
}
/**
* Die Vorschlagsliste steht einmal im Dokument, nicht in jeder Zeile.
*
* Zweitausend Einträge sind rund siebzig Kilobyte Markup; bei jedem
* Neuzeichnen einer Kachel neu zu bauen waere Verschwendung. Sie haengt
* deshalb am Ende des Dokuments und ueberlebt das Neuzeichnen der Liste.
*/
function kachelIconListeSicherstellen() {
if (document.getElementById("kachelIcons")) return;
const liste = document.createElement("datalist");
liste.id = "kachelIcons";
liste.innerHTML = (kachelModell.symbole || []).map(s => '<option value="' + s.name + '"'
+ (s.titel ? ' label="' + textSchuetzen(s.titel) + '"' : "") + ">").join("");
document.body.appendChild(liste);
}
/**
* Eine Wertdefinition als Formularzeile.
*
@@ -418,11 +476,6 @@ function kachelZeile(k) {
function kachelWertZeile(wert, i, kachel) {
const quelle = kachelQuelle(wert);
const katalog = (kachelModell && kachelModell.katalog) || [];
const symbole = kachelModell.symbole || {};
const symbolOptionen = Object.keys(symbole).map(function (key) {
return '<option value="' + key + '"' + ((wert.icon || "") === key ? " selected" : "")
+ ">" + textSchuetzen(symbole[key]) + "</option>";
}).join("");
// Eingabeart. "Messwert" steht nur zur Wahl, wenn es einen Katalog gibt -
// ohne Mesh-Datenbank waere es eine leere Liste.
@@ -516,8 +569,7 @@ function kachelWertZeile(wert, i, kachel) {
+ (wert.format === "leistung" ? " disabled" : "")
+ ' value="' + (wert.stellen === undefined ? "" : wert.stellen) + '"></div>'
+ '<div class="kw-symbol"><select class="form-select form-select-sm" data-feld="icon">'
+ symbolOptionen + "</select></div>"
+ '<div class="kw-symbol">' + kachelSymbolFeld(wert.icon || "") + "</div>"
+ '<div class="kw-schalter">'
+ '<label title="Leistung: skaliert selbst zwischen W und kW">'
@@ -584,7 +636,7 @@ function kachelWertLesen(feldZeile) {
const stellen = hol("stellen");
if (stellen !== "" && stellen !== undefined) wert.stellen = parseInt(stellen, 10);
}
const icon = hol("icon");
const icon = (hol("icon") || "").trim();
if (icon) wert.icon = icon;
if (hol("gross")) wert.gross = true;
return wert;
@@ -627,6 +679,8 @@ async function kachelnHolen() {
const d = await antwort.json();
if (!antwort.ok || d.error) throw new Error(d.error || "HTTP " + antwort.status);
kachelModell = d;
delete kachelSymbolBekannt._satz; // neue Antwort, neue Namen
kachelIconListeSicherstellen();
zeichneKacheln();
} catch (e) {
ziel.innerHTML = '<div class="alert alert-danger mb-0">Kacheln nicht abrufbar: '
@@ -715,6 +769,17 @@ function kachelnBinden() {
}
});
/*
* Die Vorschau folgt dem Tippen. Neu gezeichnet wird dafuer nicht - das
* naehme dem Feld nach jedem Buchstaben den Cursor.
*/
liste.addEventListener("input", function (ereignis) {
const feld = ereignis.target.closest('[data-feld="icon"]');
if (!feld) return;
const schau = feld.parentElement.querySelector(".kw-symbol-schau");
if (schau) schau.innerHTML = kachelSymbolVorschau(feld.value.trim());
});
const neuLaden = document.getElementById("kachelNeuLaden");
if (neuLaden) neuLaden.addEventListener("click", kachelnHolen);
}
+97 -9
View File
@@ -18,7 +18,10 @@
* einheit sonst die Einheit, die hinter der Zahl steht
* stellen Nachkommastellen (0-3)
* gross die Zahl steht gross auf der Kachel
* icon Symbol vor der Zahl, siehe kachelIcon() in rooms.php
* icon Symbol vor der Zahl: der Name eines Bootstrap-Icons ohne
* das "bi-" davor ("thermometer-half"). Erlaubt ist jedes,
* das im Stylesheet steht - siehe bootstrapIcons() in
* rooms.php; kachelSymbole() ist nur die Vorschlagsliste.
*
* Die Tabelle darf fehlen. Ist sie nicht angelegt (solarLog_kacheln.sql), so
* verhält sich alles wie vorher, statt dass die Startseite mit einem
@@ -38,17 +41,97 @@ require_once(__DIR__ . "/costs.php"); // solarDb()
/** Höchstens drei Werte je Kachel - mehr passt nicht auf 24 x 19 Einheiten. */
const KACHEL_MAX = 3;
/** Symbole, die vor einer Zahl stehen dürfen. Schlüssel wie in kachelIcon(). */
/**
* Vorschläge für das Symbol vor einer Zahl.
*
* Erlaubt ist jedes Bootstrap-Icon (bootstrapIcons() in rooms.php liest sie
* aus dem Stylesheet, es sind gut zweitausend). Diese Liste ist deshalb keine
* Beschränkung, sondern eine Abkürzung: sie steht in der Maske obenan, damit
* man für die üblichen Messwerte nicht erst einen Namen suchen muss.
*
* Gruppiert nach dem, was auf den Kacheln steht - nicht nach dem, wie die
* Symbole heißen.
*/
function kachelSymbole()
{
return [
"" => "ohne",
"ev-station" => "Ladesäule",
"cloud-rain" => "Regen",
"sun" => "Sonne",
"Klima" => [
"thermometer-half" => "Temperatur",
"thermometer-sun" => "Temperatur draußen",
"thermometer-snow" => "Kälte",
"droplet-half" => "Feuchte",
"moisture" => "Luftfeuchte",
"fan" => "Lüftung",
"fire" => "Heizung",
"wind" => "Wind",
],
"Strom" => [
"lightning-charge-fill" => "Leistung",
"plug-fill" => "Verbrauch",
"sun" => "Erzeugung",
"battery-half" => "Speicher",
"battery-charging" => "Ladung",
"ev-station" => "Wallbox",
"car-front" => "Fahrzeug",
"speedometer2" => "Zähler",
"graph-up-arrow" => "Ertrag",
"arrow-down-up" => "Bezug und Einspeisung",
],
"Wasser und Wetter" => [
"cloud-rain" => "Regen",
"cloud-sun" => "Wetter",
"snow" => "Schnee",
"water" => "Wasserstand",
"bucket" => "Menge",
"tree" => "Garten",
],
"Haus" => [
"lightbulb" => "Licht",
"door-open" => "Tür",
"window" => "Fenster",
"house-door" => "Haus",
"clock-history" => "Zeit",
"wifi" => "Verbindung",
],
];
}
/** Die Vorschläge flach, wie die Maske sie braucht: Name => Beschriftung. */
function kachelSymboleFlach()
{
$flach = [];
foreach (kachelSymbole() as $gruppe) {
foreach ($gruppe as $name => $titel) {
$flach[$name] = $titel;
}
}
return $flach;
}
/**
* Alle Symbolnamen für die Vorschlagsliste der Maske.
*
* Die Vorschläge zuerst und mit ihrer Beschriftung, danach der ganze Rest
* alphabetisch: so steht das Übliche oben, und wer etwas anderes will, tippt
* los und bekommt es angeboten.
*/
function kachelIconNamen()
{
$vorschlag = kachelSymboleFlach();
$liste = [];
foreach ($vorschlag as $name => $titel) {
if (isset(bootstrapIcons()[$name])) {
$liste[] = ["name" => $name, "titel" => $titel];
}
}
foreach (array_keys(bootstrapIcons()) as $name) {
if (!isset($vorschlag[$name])) {
$liste[] = ["name" => $name, "titel" => ""];
}
}
return $liste;
}
/**
* Alle Kacheln mit ihrem aktuellen Stand, für die Einstellungsseite.
*
@@ -262,10 +345,15 @@ function kachelWertPruefen($wert, $nummer)
}
}
$icon = (string)($wert["icon"] ?? "");
// Jedes Bootstrap-Icon darf es sein; geprüft wird gegen dieselbe Liste,
// aus der die Anzeige später das Zeichen holt. Ein Tippfehler faellt so
// beim Speichern auf und nicht erst als leere Stelle auf der Kachel.
$icon = trim((string)($wert["icon"] ?? ""));
if ($icon !== "") {
if (!array_key_exists($icon, kachelSymbole())) {
throw new InvalidArgumentException("Wert " . $nummer . ": unbekanntes Symbol.");
if (!isset(bootstrapIcons()[$icon])) {
throw new InvalidArgumentException(
"Wert " . $nummer . ": das Symbol \"" . $icon . "\" gibt es nicht. "
. "Die Namen stehen auf icons.getbootstrap.com, ohne das \"bi-\" davor.");
}
$sauber["icon"] = $icon;
}
+40 -10
View File
@@ -202,25 +202,55 @@ function floorLabel($floor)
}
/**
* Codepunkt eines Bootstrap-Icons für die Kacheln.
* Alle Bootstrap-Icons als Name => Codepunkt.
*
* Die Zahlen standen früher als Handabschrift hier - drei Stück, und jedes
* weitere Symbol war eine Codeänderung. Sie stehen aber ohnehin schon im
* Haus: css/bootstrap-icons.min.css führt sie unter .bi-<name>::before, das
* ist die Quelle, aus der auch der Browser liest. Also wird sie gelesen,
* statt sie abzuschreiben; damit steht jedes der gut zweitausend Symbole zur
* Verfügung, und ein Wechsel der Icon-Version zieht von selbst mit.
*
* Einmal je Anfrage - das Auslesen kostet rund eine Millisekunde, und die
* Startseite fragt für jede Kachel nach.
*/
function bootstrapIcons()
{
static $icons = null;
if ($icons !== null) {
return $icons;
}
$icons = [];
$css = @file_get_contents(__DIR__ . "/../css/bootstrap-icons.min.css");
if ($css === false) {
return $icons; // ohne Datei eben ohne Symbole
}
// Im CSS steht content:"\f5a2" - der Backslash gehoert zum Inhalt, im
// Muster ist er deshalb doppelt.
if (preg_match_all('/\.bi-([a-z0-9-]+)::before\{content:"\\\\([0-9a-f]+)"\}/',
$css, $treffer, PREG_SET_ORDER)) {
foreach ($treffer as $t) {
$icons[$t[1]] = hexdec($t[2]);
}
}
return $icons;
}
/**
* Das Zeichen eines Bootstrap-Icons für die Kacheln.
*
* Die Icons kommen als Schrift, nicht als Pfad: css/bootstrap-icons.min.css
* ist ohnehin eingebunden (restricted/header.php), und SVG-Text darf dieselbe
* Schriftfamilie benutzen wie HTML. Ein eingebettetes <path> je Symbol wäre
* dieselbe Wirkung mit mehr Zeilen.
*
* Die Zahlen stehen in genau dieser CSS unter .bi-<name>::before und sind von
* dort abgeschrieben - deshalb steht der Name daneben, sonst prüft das nie
* wieder jemand nach.
* Ein unbekannter Name gibt einen leeren Text - die Kachel zeigt dann die
* Zahl ohne Symbol, statt ein leeres Kästchen anzuzeigen.
*/
function kachelIcon($name)
{
$icons = [
"ev-station" => "F83A", // Wallbox, Ladeleistung
"cloud-rain" => "F2B6", // Regenmenge
"sun" => "F5A2", // PV-Erzeugung
];
return isset($icons[$name]) ? mb_chr(hexdec($icons[$name]), "UTF-8") : "";
$icons = bootstrapIcons();
return isset($icons[$name]) ? mb_chr($icons[$name], "UTF-8") : "";
}
/**