From 06b217d503338d66213820aff23fd0d3a2301311 Mon Sep 17 00:00:00 2001
From: Max Gramser
Date: Sat, 12 Sep 2026 16:58:31 +0200
Subject: [PATCH] Add visual screen layout and domain cues to tile picker
---
docs/TEST_RESULTS_024.md | 19 ++++++
screen_manager/CHANGELOG.md | 4 ++
screen_manager/app/static/app.js | 93 ++++++++++++++++++++++++++--
screen_manager/app/static/index.html | 3 +-
screen_manager/app/static/style.css | 26 ++++++++
screen_manager/config.yaml | 2 +-
6 files changed, 141 insertions(+), 6 deletions(-)
create mode 100644 docs/TEST_RESULTS_024.md
diff --git a/docs/TEST_RESULTS_024.md b/docs/TEST_RESULTS_024.md
new file mode 100644
index 0000000..bf8b578
--- /dev/null
+++ b/docs/TEST_RESULTS_024.md
@@ -0,0 +1,19 @@
+# Visuele tegelkiezer 0.2.4
+
+De beheerpagina toont een schematisch schermvoorbeeld met twee kolommen en zes
+tegels per pagina, maximaal tien tegels. Domeinen hebben herkenbare symbolen,
+kleuren en tekstlabels in de zoekresultaten, filters en het schermvoorbeeld.
+Klikken op een voorbeeldtegel opent alleen diens bestaande instellingen.
+Mini-sliders en grote waarden zijn herkenbaar; het voorbeeld toont geen live
+meetwaarden. Sleep tussen bestaande posities of gebruik de pijlen bij de
+tegelinstellingen. Toevoegen vult de eerstvolgende vrije positie.
+
+Opslag, tegelprotocol, firmware en secrets blijven ongewijzigd. Dit is uitsluitend
+een add-onupdate; de firmware blijft 0.2.3.
+
+Controles:
+- 42 Python-regressies geslaagd; JavaScript-syntax en git diff gecontroleerd.
+- Browserpreview met een kopie van de bestaande tien HA-tegels: beide pagina's,
+ domeiniconen, klik naar de juiste lampinstellingen en mini-slider-keuze getest.
+- Schermvoorbeeld en geselecteerde instellingen visueel gecontroleerd in de
+ browser. De previewserver schrijft niet naar Home Assistant.
diff --git a/screen_manager/CHANGELOG.md b/screen_manager/CHANGELOG.md
index 6b32f34..c22aeb9 100644
--- a/screen_manager/CHANGELOG.md
+++ b/screen_manager/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.2.4
+
+- Visuele tegelkiezer met domeiniconen, zachte kleuren en een klikbaar schermvoorbeeld van zes tegels per pagina. Selecteer een tegel om de instellingen te openen; sleep tegels in het voorbeeld om te ordenen. Geen firmware-update nodig.
+
## 0.2.3
- Firmware: iets donkerdere Guition-achtergrond, zachte domeinkleuren op iconen en de echte HA-lampkleur in iconen/mini-sliders. Aangepaste Lovelace-thema- en kaartkleuren worden niet automatisch geïmporteerd.
diff --git a/screen_manager/app/static/app.js b/screen_manager/app/static/app.js
index 4255a1b..cda5159 100644
--- a/screen_manager/app/static/app.js
+++ b/screen_manager/app/static/app.js
@@ -53,6 +53,7 @@ function select(id) {
)
return;
selected = id;
+ selectedTile = null;
const screen = inventory.screens.find((s) => s.id === id);
if (!screen) return;
layout = structuredClone(screen.layout);
@@ -193,13 +194,83 @@ function move(from, to) {
markDirty();
renderTiles();
}
+const domains = {
+ light: ["Licht", "☀", "#ad7600", "#fff3d3"],
+ climate: ["Klimaat", "❄", "#c86620", "#ffebdc"],
+ vacuum: ["Stofzuiger", "◉", "#008577", "#def3ed"],
+ fan: ["Ventilator", "✣", "#008aab", "#def5fa"],
+ cover: ["Zonwering", "▤", "#8053af", "#eee5f8"],
+ media_player: ["Media", "▶", "#007cad", "#def2fc"],
+ sensor: ["Sensor", "⌁", "#3476b1", "#e5effa"],
+ binary_sensor: ["Status", "◈", "#ad7600", "#fff3d3"],
+ switch: ["Schakelaar", "⏻", "#ad7600", "#fff3d3"],
+ input_boolean: ["Schakelaar", "⏻", "#ad7600", "#fff3d3"],
+ scene: ["Scène", "✦", "#8053af", "#eee5f8"],
+ script: ["Script", "▷", "#8053af", "#eee5f8"],
+ weather: ["Weer", "☁", "#007cad", "#def2fc"],
+ number: ["Waarde", "±", "#008577", "#def3ed"],
+ input_number: ["Waarde", "±", "#008577", "#def3ed"],
+ select: ["Keuze", "≡", "#5862af", "#eaecfa"],
+ input_select: ["Keuze", "≡", "#5862af", "#eaecfa"],
+ button: ["Actie", "↗", "#5862af", "#eaecfa"],
+};
+function domainBadge(id) {
+ const [title, symbol, color, background] = domains[id.split(".")[0]] || ["Entiteit", "◇", "#637184", "#edf0f4"];
+ const badge = node("span", symbol, "domain-icon");
+ badge.title = title;
+ badge.setAttribute("aria-label", title);
+ badge.style.color = color;
+ badge.style.background = background;
+ return badge;
+}
+function renderPreview() {
+ const root = $("#layout-preview");
+ root.replaceChildren();
+ const pages = Math.max(1, Math.ceil(layout.tiles.length / 6));
+ for (let page = 0; page < pages; page++) {
+ const frame = node("section", undefined, "screen-preview");
+ frame.append(node("small", `Pagina ${page + 1} · ${$("#title").value || "Thuis"}`, "preview-heading"));
+ const grid = node("div", undefined, "preview-grid");
+ for (let slot = page * 6; slot < Math.min(page * 6 + 6, 10); slot++) {
+ const tile = layout.tiles[slot];
+ const card = node("button", undefined, "preview-tile");
+ if (tile) {
+ const name = tile.name || inventory.entities.find(e => e.id === tile.entity)?.name || tile.entity;
+ card.append(domainBadge(tile.entity), node("strong", name));
+ card.draggable = true;
+ card.ondragstart = (e) => { dragIndex = slot; e.dataTransfer.setData("text/plain", String(slot)); };
+ card.ondragend = () => { dragIndex = -1; };
+ card.ondragover = (e) => { if (dragIndex >= 0) e.preventDefault(); };
+ card.ondrop = (e) => { e.preventDefault(); if (dragIndex >= 0) move(dragIndex, slot); };
+ card.classList.toggle("chosen", selectedTile === tile.entity);
+ card.setAttribute("aria-label", `Tegel ${slot + 1}: ${name}, instellen`);
+ card.onclick = () => {
+ selectedTile = tile.entity;
+ renderTiles();
+ document.querySelectorAll("#tiles > .tile")[slot]?.scrollIntoView({behavior: "smooth", block: "center"});
+ };
+ if (tile.options?.inline === "slider") card.append(node("span", "", "preview-slider"));
+ if (tile.options?.display === "watch") card.append(node("small", "Grote waarde"));
+ } else {
+ card.classList.add("vacant");
+ card.append(node("span", "+"), node("small", "Tegel toevoegen"));
+ card.onclick = () => { $("#search").focus(); $("#search").scrollIntoView({behavior:"smooth", block:"center"}); };
+ }
+ grid.append(card);
+ }
+ frame.append(grid);
+ root.append(frame);
+ }
+}
function renderTiles() {
+ renderPreview();
$("#tiles").replaceChildren();
$("#count").textContent = `${layout.tiles.length} / 10`;
$("#no-tiles").hidden = layout.tiles.length > 0;
layout.tiles.forEach((tile, i) => {
if (i === 6) $("#tiles").append(node("li", "Pagina 2", "page-break"));
const li = node("li", undefined, "tile");
+ li.classList.toggle("selected-tile", selectedTile === tile.entity);
li.draggable = true;
li.ondragstart = (e) => {
if (e.target.closest("input, select, button")) {
@@ -231,6 +302,7 @@ function renderTiles() {
input.oninput = () => {
tile.name = input.value;
markDirty();
+ renderPreview();
};
content.append(input, node("small", tile.entity));
const actions = node("div", undefined, "tile-actions");
@@ -263,6 +335,7 @@ function renderTiles() {
li.classList.toggle("selected-tile", options.open);
if (options.open) {
selectedTile = tile.entity;
+ renderPreview();
document.querySelectorAll(".tile-options").forEach((other) => {
if (other !== options) other.open = false;
});
@@ -359,6 +432,7 @@ function renderTiles() {
control.value = tile.options[field];
}
markDirty();
+ renderPreview();
};
label.append(input);
options.append(label);
@@ -367,7 +441,7 @@ function renderTiles() {
inspectTile.onclick = () => inspect(tile.entity);
options.append(inspectTile);
content.append(options);
- li.append(node("span", "⠿", "grip"), content, actions);
+ li.append(domainBadge(tile.entity), content, actions);
$("#tiles").append(li);
});
}
@@ -377,7 +451,8 @@ function renderResults() {
const chosen = new Set(layout.tiles.map((t) => t.entity));
const matches = inventory.entities.filter(
(e) =>
- (!filter || e.id.startsWith(filter + ".")) &&
+ (!filter || e.id.startsWith(filter + ".") ||
+ ({switch:"input_boolean", number:"input_number", select:"input_select"}[filter] === e.id.split(".")[0])) &&
`${e.name} ${e.id} ${e.device} ${e.area}`
.toLocaleLowerCase()
.includes(query),
@@ -387,17 +462,20 @@ function renderResults() {
const b = node("button", undefined, "result"),
description = node("span");
description.append(
- node("span", entity.name),
+ node("strong", entity.name),
+ node("small", domains[entity.id.split(".")[0]]?.[0] || entity.id.split(".")[0], "domain-label"),
node("small", [entity.area, entity.device].filter(Boolean).join(" · ")),
node("small", entity.id),
);
b.append(
+ domainBadge(entity.id),
description,
node("span", chosen.has(entity.id) ? "✓" : "+", "plus"),
);
b.disabled = chosen.has(entity.id) || layout.tiles.length >= 10;
b.onclick = () => {
layout.tiles.push({ entity: entity.id, name: "" });
+ selectedTile = entity.id;
markDirty();
renderTiles();
renderResults();
@@ -458,13 +536,19 @@ $("#save").onclick = async () => {
$("#save").disabled = false;
}
};
-$("#title").oninput = markDirty;
+$("#title").oninput = () => { markDirty(); renderPreview(); };
$("#search").oninput = renderResults;
$("#refresh").onclick = refresh;
for (const [value, label] of [
["", "Alles"],
["light", "Lampen"],
["climate", "Klimaat"],
+ ["switch", "Schakelaars"],
+ ["binary_sensor", "Status"],
+ ["button", "Acties"],
+ ["script", "Scripts"],
+ ["fan", "Ventilatoren"],
+ ["cover", "Zonwering"],
["scene", "Scènes"],
["vacuum", "Vacuum"],
["sensor", "Sensoren"],
@@ -474,6 +558,7 @@ for (const [value, label] of [
["select", "Keuzelijsten"],
]) {
const b = node("button", label, value === "" ? "active" : "");
+ if (value) b.prepend(domainBadge(value + "."));
b.onclick = () => {
filter = value;
$("#filters")
diff --git a/screen_manager/app/static/index.html b/screen_manager/app/static/index.html
index 1d747f4..10fc945 100644
--- a/screen_manager/app/static/index.html
+++ b/screen_manager/app/static/index.html
@@ -77,9 +77,10 @@
Klik op een tegel om bediening en weergave te kiezen. Sleep om
te ordenen. Zes tegels per pagina.
+
- Voeg rechts je eerste lamp, scène of apparaat toe.
+ Voeg je eerste lamp, scène of apparaat toe via de kiezer.
diff --git a/screen_manager/app/static/style.css b/screen_manager/app/static/style.css
index 9a85051..0333b93 100644
--- a/screen_manager/app/static/style.css
+++ b/screen_manager/app/static/style.css
@@ -694,3 +694,29 @@ pre {
#tile-section {
scroll-margin-top: 16px;
}
+
+/* The picker shares the screen's two-column, six-tile page structure. */
+.domain-icon { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; flex: 0 0 36px; border-radius: 50%; font-size: 22px; font-weight: 500; }
+.tile { grid-template-columns: 36px minmax(0, 1fr) auto; }
+.result { border: 1px solid var(--line); border-radius: 14px; background: white; margin-bottom: 8px; padding: 12px; }
+.result > span:nth-child(2) { flex: 1; min-width: 0; }
+.result strong { display: block; }
+.result .domain-label { display: inline-block; margin: 3px 0; color: var(--muted); }
+.filters button { display: inline-flex; align-items: center; gap: 5px; }
+.filters .domain-icon { width: 20px; height: 20px; flex-basis: 20px; font-size: 15px; }
+.screen-preview { background: #e7e7e7; border: 1px solid #d9dde2; border-radius: 22px; padding: 14px; margin-top: 14px; }
+.preview-heading { display: block; color: #49515a; margin: 0 2px 12px; }
+.preview-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 9px; }
+.preview-tile { background: white; color: #25282c; border: 1px solid #ddd; border-radius: 15px; padding: 12px; min-height: 90px; text-align: left; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
+.preview-tile strong { flex: 1; min-width: 0; font-size: 13px; overflow-wrap: anywhere; }
+.preview-tile .domain-icon { width: 30px; height: 30px; flex-basis: 30px; font-size: 20px; }
+.preview-tile.chosen { outline: 2px solid var(--accent); outline-offset: 1px; }
+.preview-tile.vacant { background: #f4f4f4; border-style: dashed; color: #727982; justify-content: center; }
+.preview-slider { height: 13px; flex-basis: 100%; border-radius: 10px; background: linear-gradient(to right, #ffbf38 60%, #fff1d3 60%); }
+.preview-tile small { flex-basis: 100%; }
+.result:hover:not(:disabled), .preview-tile:hover { border-color: #9eb6cf; box-shadow: 0 2px 7px #253f5010; }
+
+#tiles > .tile:not(.selected-tile), #tiles > .page-break { display: none; }
+#tiles > .selected-tile { background: #f8fafc; border-color: #aac4df; }
+.picker { position: sticky; top: 16px; }
+@media (max-width: 800px) { .picker { position: static; } }
diff --git a/screen_manager/config.yaml b/screen_manager/config.yaml
index 516b42a..7246900 100644
--- a/screen_manager/config.yaml
+++ b/screen_manager/config.yaml
@@ -1,5 +1,5 @@
name: ESP Screen Manager
-version: "0.2.3"
+version: "0.2.4"
slug: esp_screen_manager
description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen.
url: https://github.com/MaxGramser/homeassistant_espscreen