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