diff --git a/docs/TEST_RESULTS_0215.md b/docs/TEST_RESULTS_0215.md index 9f89cc8..9faa886 100644 --- a/docs/TEST_RESULTS_0215.md +++ b/docs/TEST_RESULTS_0215.md @@ -53,3 +53,14 @@ CYD 10/10 (`count=7`, ~52,5 kB vrij). Beide handmatige profielen compileren. De skeletonwissel en de CYD-kleuren zijn door de eigenaar visueel te beoordelen; er is geen langdurige stabiliteitsproef uitgevoerd. + +## App 0.2.14 (zelfde firmware) + +Tegelinstellingen openen nu in een sheet boven de mockup (mobiel: onderaan) +met knoppen voor naam, weergave, breedte, tikgedrag, slider, geschiedenis en +kleur; elke keuze werkt live door in de mockup erachter. Het kruisje op een +kaart verwijdert direct, met **Ongedaan maken** in de melding; de tegellijst +onder de mockup is vervallen. In de browser met de gemockte inventory +gecontroleerd: sheet-layout, weergave "Grote waarde" direct zichtbaar in de +mockup, verwijderen en ongedaan maken herstellen de volgorde, het kruisje +start geen sleep. 55 Python-tests geslaagd; geen firmwarewijziging. diff --git a/screen_manager/CHANGELOG.md b/screen_manager/CHANGELOG.md index 90799c3..5510b31 100644 --- a/screen_manager/CHANGELOG.md +++ b/screen_manager/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.2.14 (firmware 0.2.15 blijft actueel) + +- Tegelinstellingen openen in een paneel bóven de schermmockup (op mobiel een sheet onderaan): naam, weergave, breedte, tikgedrag, slider, geschiedenis en kleur als knoppen, direct zichtbaar in de mockup erachter. Geen springende pagina meer. +- Verwijderen kan direct in de mockup met het kruisje op een tegel; de melding onderaan heeft **Ongedaan maken**. +- De tegellijst onder de mockup is vervallen; ordenen doe je door te slepen. + ## 0.2.13 (firmware 0.2.15) - Editor: sleep tegels in de schermmockup om te ordenen en sleep entiteiten rechtstreeks uit de lijst naar een plek in de mockup — met muis én touch (even vasthouden). De opslaan-balk blijft altijd in beeld. diff --git a/screen_manager/app/static/app.js b/screen_manager/app/static/app.js index ca8d0b7..59ce93d 100644 --- a/screen_manager/app/static/app.js +++ b/screen_manager/app/static/app.js @@ -13,11 +13,18 @@ const node = (tag, text, cls) => { if (cls) n.className = cls; return n; }; -function toast(message) { - $("#toast").textContent = message; - $("#toast").hidden = false; +function toast(message, action) { + const t = $("#toast"); + t.replaceChildren(node("span", message)); + if (action) { + const b = node("button", action.label); + b.type = "button"; + b.onclick = () => { action.run(); t.hidden = true; }; + t.append(b); + } + t.hidden = false; clearTimeout(toast.timer); - toast.timer = setTimeout(() => ($("#toast").hidden = true), 5000); + toast.timer = setTimeout(() => (t.hidden = true), action ? 8000 : 5000); } async function api(path, options = {}) { const response = await fetch(`api/${path}`, { @@ -53,6 +60,7 @@ function select(id) { return; selected = id; selectedTile = null; + if ($("#tile-sheet").open) $("#tile-sheet").close(); const screen = inventory.screens.find((s) => s.id === id); if (!screen) return; layout = structuredClone(screen.layout); @@ -284,7 +292,9 @@ function renderPreview() { if (place.page !== page) return; // Keep the grid honest: an empty right column before a wide tile is a real gap on the screen. for (; filled < place.slot; filled++) grid.append(node("span", "", "preview-gap")); - const card = node("button", undefined, "preview-tile"); + const card = node("div", undefined, "preview-tile"); + card.tabIndex = 0; + card.setAttribute("role", "button"); const name = tile.name || entityName(tile.entity); const background=inventory.backgrounds?.[tile.options?.background]?.color; if(background)card.style.backgroundColor=background; @@ -294,11 +304,14 @@ function renderPreview() { enableDrag(card, { kind: "tile", index }); card.classList.toggle("chosen", selectedTile === tile.entity); card.setAttribute("aria-label", `Tegel ${index + 1}: ${name}, instellen`); - card.onclick = () => { - selectedTile = tile.entity; - renderTiles(); - document.querySelectorAll("#tiles > .tile")[index]?.scrollIntoView({behavior: "smooth", block: "center"}); - }; + card.onclick = () => openTileSheet(index); + card.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openTileSheet(index); } }; + const remove = node("button", "✕", "preview-remove"); + remove.type = "button"; + remove.title = "Tegel verwijderen"; + remove.setAttribute("aria-label", `${name} verwijderen`); + remove.onclick = (e) => { e.stopPropagation(); removeTile(index); }; + card.append(remove); if (tile.options?.inline === "slider") card.append(node("span", "", "preview-slider")); const display = tile.options?.display; if (display && display !== "standard") card.append(node("small", displayNames[display] || display)); @@ -318,193 +331,140 @@ function renderPreview() { } function renderTiles() { renderPreview(); - $("#tiles").replaceChildren(); $("#count").textContent = `${layout.tiles.length} / ${tileLimit()}${tileLimit()===10?" · update firmware voor 20":""}`; $("#no-tiles").hidden = layout.tiles.length > 0; - const { placement } = packTiles(layout.tiles); - layout.tiles.forEach((tile, i) => { - if (i > 0 && placement[i].page !== placement[i - 1].page) $("#tiles").append(node("li", `Pagina ${placement[i].page + 1}`, "page-break")); - const li = node("li", undefined, "tile"); - li.classList.toggle("selected-tile", selectedTile === tile.entity); - const content = node("div"), - input = node("input"); - input.value = tile.name; - input.placeholder = entityName(tile.entity); - input.maxLength = 60; - input.setAttribute("aria-label", `Naam voor tegel ${i + 1}`); - input.oninput = () => { - tile.name = input.value; + if (sheetIndex >= 0) renderTileSheet(); +} +function removeTile(index) { + const [tile] = layout.tiles.splice(index, 1); + if (!tile) return; + if (sheetIndex >= 0) closeTileSheet(); + markDirty(); + renderTiles(); + renderResults(); + toast(`${tile.name || entityName(tile.entity)} verwijderd`, { + label: "Ongedaan maken", + run: () => { layout.tiles.splice(Math.min(index, layout.tiles.length), 0, tile); markDirty(); renderTiles(); renderResults(); }, + }); +} +// Tile settings open in a sheet above the mockup; every change applies live, +// so the card behind it shows the result while you pick. +let sheetIndex = -1; +function openTileSheet(index) { + sheetIndex = index; + selectedTile = layout.tiles[index].entity; + renderTileSheet(); + renderPreview(); + const sheet = $("#tile-sheet"); + if (!sheet.open) sheet.showModal(); +} +function closeTileSheet() { + sheetIndex = -1; + selectedTile = null; + const sheet = $("#tile-sheet"); + if (sheet.open) sheet.close(); + renderPreview(); +} +$("#tile-sheet").addEventListener("close", () => { if (sheetIndex >= 0) { sheetIndex = -1; selectedTile = null; renderPreview(); } }); +function field(title, control) { + const label = node("label", undefined, "sheet-field"); + label.append(node("span", title), control); + return label; +} +function segmented(choices, value, onChange) { + const group = node("div", undefined, "segmented"); + for (const [key, text] of choices) { + const b = node("button", text); + b.type = "button"; + b.setAttribute("aria-pressed", String(String(key) === String(value))); + b.onclick = () => { for (const other of group.children) other.setAttribute("aria-pressed", String(other === b)); onChange(key); }; + group.append(b); + } + return group; +} +function renderTileSheet() { + const sheet = $("#tile-sheet"), tile = layout.tiles[sheetIndex]; + if (!tile) return; + sheet.replaceChildren(); + const domain = tile.entity.split(".")[0], name = entityName(tile.entity); + const head = node("div", undefined, "sheet-head"), titles = node("div"); + titles.append(node("strong", name), node("small", tile.entity)); + const close = node("button", "✕", "quiet sheet-close"); + close.type = "button"; + close.setAttribute("aria-label", "Sluiten"); + close.onclick = closeTileSheet; + head.append(domainBadge(tile.entity), titles, close); + const body = node("div", undefined, "sheet-body"); + const nameInput = node("input"); + nameInput.value = tile.name; + nameInput.placeholder = name; + nameInput.maxLength = 60; + nameInput.oninput = () => { tile.name = nameInput.value; markDirty(); renderPreview(); }; + body.append(field("Naam op het scherm", nameInput)); + const displays = domain === "screen" + ? [["digital", "Digitale klok"], ["analog", "Analoge klok"]] + : [["standard", "Naam en status"], ["watch", "Grote waarde"]]; + if (domain === "weather") displays.push(["forecast", "Weersvoorspelling"]); + if (domain === "sensor") displays.push(["graph", "Grafiek"]); + if (domain === "sun") displays.push(["sunpath", "Zonnebaan"]); + const current = (key, fallback) => tile.options?.[key] ?? fallback; + const set = (key, value) => { + tile.options = { ...tile.options, [key]: value }; + if (key === "display" && value === "watch") tile.options.inline = "none"; + if (key === "display" && ["forecast", "sunpath"].includes(value)) tile.options.size = "wide"; + if (key === "inline" && value === "slider") tile.options.display = "standard"; + markDirty(); + renderTiles(); + }; + body.append(field("Weergave", segmented(displays, current("display", domain === "screen" ? "digital" : "standard"), (v) => set("display", v)))); + body.append(field("Breedte", segmented([["single", "Normaal"], ["wide", "Dubbelbreed"]], current("size", "single"), (v) => set("size", v)))); + if (domain !== "screen") { + const taps = [["auto", "Automatisch"], ["detail", "Bediening openen"], ["none", "Alleen bekijken"]]; + if (["light", "switch", "input_boolean", "fan", "media_player"].includes(domain)) taps.push(["toggle", "Aan / uit"]); + body.append(field("Bij aantikken", segmented(taps, current("tap", "auto"), (v) => set("tap", v)))); + } + if (["light", "fan", "cover", "number", "input_number", "media_player"].includes(domain)) + body.append(field("Kleine slider op de tegel", segmented([["none", "Nee"], ["slider", "Ja, direct bedienen"]], current("inline", "none"), (v) => set("inline", v)))); + if (domain === "sensor") + body.append(field("Geschiedenis", segmented([[1, "1 uur"], [6, "6 uur"], [24, "24 uur"]], current("history_hours", 24), (v) => set("history_hours", Number(v))))); + const palette = node("div", undefined, "sheet-field"); + palette.append(node("span", "Pastel achtergrond")); + const swatches = node("div", undefined, "palette-swatches"); + for (const [key, choice] of Object.entries(inventory.backgrounds || {})) { + const button = node("button", undefined, "palette-choice"); + button.type = "button"; + button.setAttribute("aria-label", `Achtergrond: ${choice.label}`); + button.setAttribute("aria-pressed", String((tile.options?.background || "auto") === key)); + const sample = node("span", undefined, "palette-sample"); + if (choice.color) sample.style.backgroundColor = choice.color; + else sample.classList.add("palette-auto"); + button.append(sample, node("span", choice.label)); + button.onclick = () => { + tile.options = { ...tile.options, background: key }; + for (const other of swatches.children) other.setAttribute("aria-pressed", String(other === button)); markDirty(); renderPreview(); }; - content.append(input, node("small", tile.entity)); - const actions = node("div", undefined, "tile-actions"); - for (const [label, title, action, disabled] of [ - ["↑", "Omhoog", () => move(i, i - 1), i === 0], - ["↓", "Omlaag", () => move(i, i + 1), i === layout.tiles.length - 1], - [ - "✕", - "Verwijderen", - () => { - layout.tiles.splice(i, 1); - markDirty(); - renderTiles(); - renderResults(); - }, - false, - ], - ]) { - const b = node("button", label); - b.title = title; - b.setAttribute("aria-label", title); - b.disabled = disabled; - b.onclick = action; - actions.append(b); - } - const options = node("details", undefined, "tile-options"); - options.append(node("summary", "Bediening & weergave instellen")); - options.open = selectedTile === tile.entity; - options.ontoggle = () => { - 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; - }); - } - }; - li.onclick = (e) => { - if (!e.target.closest("input, select, button, summary, .tile-options")) - options.open = !options.open; - }; - const palette = node("fieldset", undefined, "tile-palette"); - palette.append(node("legend", "Pastel achtergrond")); - const swatches = node("div", undefined, "palette-swatches"); - for(const [key, choice] of Object.entries(inventory.backgrounds || {})) { - const button=node("button", undefined, "palette-choice"); - button.type="button"; - button.setAttribute("aria-label", `Achtergrond: ${choice.label}`); - button.setAttribute("aria-pressed", String((tile.options?.background || "auto") === key)); - const sample=node("span", undefined, "palette-sample"); - if(choice.color)sample.style.backgroundColor=choice.color; - else sample.classList.add("palette-auto"); - button.append(sample,node("span",choice.label)); - button.onclick=()=>{ - tile.options={...tile.options,background:key}; - for(const other of swatches.children)other.setAttribute("aria-pressed",String(other===button)); - markDirty();renderPreview(); - }; - swatches.append(button); - } - palette.append(swatches,node("small", "Donkere tekst blijft leesbaar. Kies Standaard voor de normale kleuren. Vereist schermfirmware 0.2.10+.")); - options.append(palette); - const controls = {}; - const domain = tile.entity.split(".")[0]; - const displays = domain === "screen" - ? [["digital", "Digitale klok"], ["analog", "Analoge klok"]] - : [["standard", "Naam en status"], ["watch", "Grote waarde"]]; - if (domain === "weather") displays.push(["forecast", "Weersvoorspelling (dubbelbreed)"]); - if (domain === "sensor") displays.push(["graph", "Grafiek van de geschiedenis"]); - if (domain === "sun") displays.push(["sunpath", "Zonnebaan (dubbelbreed)"]); - const fields = [ - [ - "tap", - "Bij aantikken", - [ - ["auto", "Automatisch"], - ["detail", "Bediening openen"], - ["none", "Alleen bekijken"], - ], - ], - ["display", "Weergave", displays], - ["size", "Breedte (firmware 0.2.14+)", [["single", "Normaal"], ["wide", "Dubbelbreed"]]], - ]; - if (domain === "screen") fields.shift(); - if ( - ["light", "switch", "input_boolean", "fan", "media_player"].includes( - domain, - ) - ) - fields[0][2].push(["toggle", "Aan / uit"]); - if ( - [ - "light", - "fan", - "cover", - "number", - "input_number", - "media_player", - ].includes(domain) - ) - fields.push([ - "inline", - "Kleine slider op deze tegel?", - [ - ["none", "Nee"], - ["slider", "Ja, direct bedienen"], - ], - ]); - if (domain === "sensor") - fields.push([ - "history_hours", - "Geschiedenis", - [ - [1, "1 uur"], - [6, "6 uur"], - [24, "24 uur"], - ], - ]); - for (const [key, title, choices] of fields) { - const label = node("label", title), - input = node("select"); - for (const [value, text] of choices) { - const option = node("option", text); - option.value = value; - input.append(option); - } - input.value = - tile.options?.[key] ?? - (key === "tap" - ? "auto" - : key === "display" - ? (domain === "screen" ? "digital" : "standard") - : key === "history_hours" - ? 24 - : key === "size" - ? "single" - : "none"); - controls[key] = input; - input.onchange = () => { - tile.options = { - ...tile.options, - [key]: key === "history_hours" ? Number(input.value) : input.value, - }; - if (key === "display" && input.value === "watch") - tile.options.inline = "none"; - if (key === "display" && ["forecast", "sunpath"].includes(input.value)) - tile.options.size = "wide"; - if (key === "inline" && input.value === "slider") - tile.options.display = "standard"; - for (const [field, control] of Object.entries(controls)) { - if (tile.options[field] !== undefined) - control.value = tile.options[field]; - } - markDirty(); - renderPreview(); - }; - label.append(input); - options.append(label); - } - if (domain !== "screen") { - const inspectTile = node("button", "Inspecteer deze tegel", "quiet"); - inspectTile.onclick = () => inspect(tile.entity); - options.append(inspectTile); - } - content.append(options); - li.append(domainBadge(tile.entity), content, actions); - $("#tiles").append(li); - }); + swatches.append(button); + } + palette.append(swatches); + body.append(palette); + const foot = node("div", undefined, "sheet-foot"); + const remove = node("button", "Verwijderen", "quiet danger"); + remove.type = "button"; + remove.onclick = () => removeTile(sheetIndex); + foot.append(remove); + if (domain !== "screen") { + const inspectButton = node("button", "Inspecteer", "quiet"); + inspectButton.type = "button"; + inspectButton.onclick = () => { const entity = tile.entity; closeTileSheet(); inspect(entity); }; + foot.append(inspectButton); + } + const done = node("button", "Klaar"); + done.type = "button"; + done.onclick = closeTileSheet; + foot.append(done); + sheet.append(head, body, foot); } function addTile(id, at) { if (layout.tiles.some((t) => t.entity === id) || layout.tiles.length >= tileLimit()) return; @@ -520,7 +480,7 @@ function addTile(id, at) { const drag = { active: false, source: null, element: null, ghost: null, target: null, timer: 0, start: null, offset: null, pointerId: null, suppressUntil: 0, last: null, scroller: 0 }; function enableDrag(element, source) { element.addEventListener("pointerdown", (e) => { - if (e.button !== 0 || element.disabled) return; + if (e.button !== 0 || element.disabled || e.target.closest(".preview-remove")) return; Object.assign(drag, { source, element, start: { x: e.clientX, y: e.clientY }, pointerId: e.pointerId, active: false }); clearTimeout(drag.timer); if (e.pointerType === "touch") drag.timer = setTimeout(() => beginDrag(e), 260); diff --git a/screen_manager/app/static/index.html b/screen_manager/app/static/index.html index e28fa7d..06dac38 100644 --- a/screen_manager/app/static/index.html +++ b/screen_manager/app/static/index.html @@ -112,12 +112,12 @@ 0 / 10

- Klik op een tegel om bediening, weergave en breedte te kiezen. - Sleep om te ordenen. Zes vakjes per pagina; een dubbelbrede - tegel telt voor twee en begint altijd links. + Klik op een tegel voor naam, weergave, breedte en kleur; het + kruisje verwijdert. Sleep om te ordenen of sleep entiteiten uit + de lijst hierheen. Zes vakjes per pagina; een dubbelbrede tegel + telt voor twee en begint altijd links.

-

    Voeg je eerste lamp, scène of apparaat toe via de kiezer.

    @@ -351,6 +351,7 @@

    
         
    +    
         
       
     
    diff --git a/screen_manager/app/static/style.css b/screen_manager/app/static/style.css
    index 520c61b..d5c9aff 100644
    --- a/screen_manager/app/static/style.css
    +++ b/screen_manager/app/static/style.css
    @@ -544,6 +544,9 @@ dialog label {
       bottom: 24px;
       left: 50%;
       transform: translateX(-50%);
    +  display: flex;
    +  align-items: center;
    +  gap: 16px;
       padding: 14px 22px;
       background: var(--ink);
       color: white;
    @@ -842,3 +845,29 @@ pre {
     .palette-sample { width: 36px; height: 28px; border-radius: 7px; border: 1px solid #0002; }
     .palette-auto { background: linear-gradient(135deg, #fff 50%, #e5e7eb 50%); }
     .tile-palette small { color: #52616e; }
    +
    +/* Tile settings sheet and in-preview removal. */
    +#toast button { background: transparent; color: #9fc4ff; padding: 0; font-weight: 650; }
    +.preview-tile { position: relative; }
    +.preview-remove { position: absolute; top: 6px; right: 6px; width: 24px; height: 24px; padding: 0; border-radius: 50%; background: #ffffffd9; color: var(--muted); border: 1px solid var(--line); font-size: 11px; line-height: 1; display: grid; place-items: center; opacity: 0; transition: opacity 0.15s; }
    +.preview-tile:hover .preview-remove, .preview-tile:focus-within .preview-remove { opacity: 1; }
    +.preview-remove:hover { color: #b3261e; border-color: #e7b4b0; background: white; }
    +@media (hover: none) { .preview-remove { opacity: 1; } }
    +.tile-sheet { padding: 0; width: min(560px, calc(100% - 28px)); overflow: hidden; }
    +.sheet-head { display: flex; align-items: center; gap: 12px; padding: 16px 20px; border-bottom: 1px solid var(--line); }
    +.sheet-head > div { flex: 1; min-width: 0; }
    +.sheet-head strong { display: block; font-size: 16px; }
    +.sheet-head small { color: var(--muted); overflow-wrap: anywhere; }
    +.sheet-close { font-size: 15px; }
    +.sheet-body { padding: 16px 20px; display: grid; gap: 14px; max-height: 58vh; overflow: auto; }
    +.sheet-field { display: grid; gap: 7px; font-size: 12px; font-weight: 650; color: var(--muted); }
    +.sheet-field input { font-weight: 400; font-size: 14px; }
    +.segmented { display: flex; flex-wrap: wrap; gap: 6px; }
    +.segmented button { background: #f3f5f8; color: var(--ink); border: 1px solid transparent; border-radius: 10px; padding: 8px 12px; font-size: 13px; font-weight: 500; }
    +.segmented button[aria-pressed="true"] { background: var(--soft); color: var(--accent); border-color: #c9dafa; font-weight: 650; }
    +.sheet-foot { display: flex; gap: 10px; align-items: center; padding: 14px 20px; border-top: 1px solid var(--line); background: #fafbfc; }
    +.sheet-foot .danger { color: #b3261e; margin-right: auto; }
    +@media (max-width: 620px) {
    +  .tile-sheet { width: 100%; max-width: 100%; margin: auto 0 0; border-radius: 20px 20px 0 0; max-height: 90vh; }
    +  .sheet-body { max-height: 52vh; }
    +}
    diff --git a/screen_manager/config.yaml b/screen_manager/config.yaml
    index fbc9757..be1619b 100644
    --- a/screen_manager/config.yaml
    +++ b/screen_manager/config.yaml
    @@ -1,5 +1,5 @@
     name: ESP Screen Manager
    -version: "0.2.13"
    +version: "0.2.14"
     slug: esp_screen_manager
     description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen.
     url: https://github.com/MaxGramser/homeassistant_espscreen