Add visual screen layout and domain cues to tile picker

This commit is contained in:
Max Gramser
2026-09-12 16:58:31 +02:00
parent a34d7a3792
commit 06b217d503
6 changed files with 141 additions and 6 deletions
+19
View File
@@ -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.
+4
View File
@@ -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.
+89 -4
View File
@@ -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")
+2 -1
View File
@@ -77,9 +77,10 @@
Klik op een tegel om bediening en weergave te kiezen. Sleep om
te ordenen. Zes tegels per pagina.
</p>
<div id="layout-preview" aria-label="Schermindeling"></div>
<ol id="tiles"></ol>
<p id="no-tiles" class="hint">
Voeg rechts je eerste lamp, scène of apparaat toe.
Voeg je eerste lamp, scène of apparaat toe via de kiezer.
</p>
</div>
<div class="picker">
+26
View File
@@ -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; } }
+1 -1
View File
@@ -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