Open tile settings in a sheet above the mockup and remove tiles in place
App 0.2.14: clicking a card opens a settings sheet (bottom sheet on mobile) with segmented choices that apply live to the mockup behind it, so the page no longer scrolls to a list below. Each card gets a remove button with an undo toast; the old per-tile list is gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -53,3 +53,14 @@
|
|||||||
CYD 10/10 (`count=7`, ~52,5 kB vrij). Beide handmatige profielen compileren.
|
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
|
De skeletonwissel en de CYD-kleuren zijn door de eigenaar visueel te
|
||||||
beoordelen; er is geen langdurige stabiliteitsproef uitgevoerd.
|
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.
|
||||||
|
|||||||
@@ -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)
|
## 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.
|
- 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.
|
||||||
|
|||||||
+153
-193
@@ -13,11 +13,18 @@ const node = (tag, text, cls) => {
|
|||||||
if (cls) n.className = cls;
|
if (cls) n.className = cls;
|
||||||
return n;
|
return n;
|
||||||
};
|
};
|
||||||
function toast(message) {
|
function toast(message, action) {
|
||||||
$("#toast").textContent = message;
|
const t = $("#toast");
|
||||||
$("#toast").hidden = false;
|
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);
|
clearTimeout(toast.timer);
|
||||||
toast.timer = setTimeout(() => ($("#toast").hidden = true), 5000);
|
toast.timer = setTimeout(() => (t.hidden = true), action ? 8000 : 5000);
|
||||||
}
|
}
|
||||||
async function api(path, options = {}) {
|
async function api(path, options = {}) {
|
||||||
const response = await fetch(`api/${path}`, {
|
const response = await fetch(`api/${path}`, {
|
||||||
@@ -53,6 +60,7 @@ function select(id) {
|
|||||||
return;
|
return;
|
||||||
selected = id;
|
selected = id;
|
||||||
selectedTile = null;
|
selectedTile = null;
|
||||||
|
if ($("#tile-sheet").open) $("#tile-sheet").close();
|
||||||
const screen = inventory.screens.find((s) => s.id === id);
|
const screen = inventory.screens.find((s) => s.id === id);
|
||||||
if (!screen) return;
|
if (!screen) return;
|
||||||
layout = structuredClone(screen.layout);
|
layout = structuredClone(screen.layout);
|
||||||
@@ -284,7 +292,9 @@ function renderPreview() {
|
|||||||
if (place.page !== page) return;
|
if (place.page !== page) return;
|
||||||
// Keep the grid honest: an empty right column before a wide tile is a real gap on the screen.
|
// 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"));
|
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 name = tile.name || entityName(tile.entity);
|
||||||
const background=inventory.backgrounds?.[tile.options?.background]?.color;
|
const background=inventory.backgrounds?.[tile.options?.background]?.color;
|
||||||
if(background)card.style.backgroundColor=background;
|
if(background)card.style.backgroundColor=background;
|
||||||
@@ -294,11 +304,14 @@ function renderPreview() {
|
|||||||
enableDrag(card, { kind: "tile", index });
|
enableDrag(card, { kind: "tile", index });
|
||||||
card.classList.toggle("chosen", selectedTile === tile.entity);
|
card.classList.toggle("chosen", selectedTile === tile.entity);
|
||||||
card.setAttribute("aria-label", `Tegel ${index + 1}: ${name}, instellen`);
|
card.setAttribute("aria-label", `Tegel ${index + 1}: ${name}, instellen`);
|
||||||
card.onclick = () => {
|
card.onclick = () => openTileSheet(index);
|
||||||
selectedTile = tile.entity;
|
card.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openTileSheet(index); } };
|
||||||
renderTiles();
|
const remove = node("button", "✕", "preview-remove");
|
||||||
document.querySelectorAll("#tiles > .tile")[index]?.scrollIntoView({behavior: "smooth", block: "center"});
|
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"));
|
if (tile.options?.inline === "slider") card.append(node("span", "", "preview-slider"));
|
||||||
const display = tile.options?.display;
|
const display = tile.options?.display;
|
||||||
if (display && display !== "standard") card.append(node("small", displayNames[display] || display));
|
if (display && display !== "standard") card.append(node("small", displayNames[display] || display));
|
||||||
@@ -318,193 +331,140 @@ function renderPreview() {
|
|||||||
}
|
}
|
||||||
function renderTiles() {
|
function renderTiles() {
|
||||||
renderPreview();
|
renderPreview();
|
||||||
$("#tiles").replaceChildren();
|
|
||||||
$("#count").textContent = `${layout.tiles.length} / ${tileLimit()}${tileLimit()===10?" · update firmware voor 20":""}`;
|
$("#count").textContent = `${layout.tiles.length} / ${tileLimit()}${tileLimit()===10?" · update firmware voor 20":""}`;
|
||||||
$("#no-tiles").hidden = layout.tiles.length > 0;
|
$("#no-tiles").hidden = layout.tiles.length > 0;
|
||||||
const { placement } = packTiles(layout.tiles);
|
if (sheetIndex >= 0) renderTileSheet();
|
||||||
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"));
|
function removeTile(index) {
|
||||||
const li = node("li", undefined, "tile");
|
const [tile] = layout.tiles.splice(index, 1);
|
||||||
li.classList.toggle("selected-tile", selectedTile === tile.entity);
|
if (!tile) return;
|
||||||
const content = node("div"),
|
if (sheetIndex >= 0) closeTileSheet();
|
||||||
input = node("input");
|
markDirty();
|
||||||
input.value = tile.name;
|
renderTiles();
|
||||||
input.placeholder = entityName(tile.entity);
|
renderResults();
|
||||||
input.maxLength = 60;
|
toast(`${tile.name || entityName(tile.entity)} verwijderd`, {
|
||||||
input.setAttribute("aria-label", `Naam voor tegel ${i + 1}`);
|
label: "Ongedaan maken",
|
||||||
input.oninput = () => {
|
run: () => { layout.tiles.splice(Math.min(index, layout.tiles.length), 0, tile); markDirty(); renderTiles(); renderResults(); },
|
||||||
tile.name = input.value;
|
});
|
||||||
|
}
|
||||||
|
// 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();
|
markDirty();
|
||||||
renderPreview();
|
renderPreview();
|
||||||
};
|
};
|
||||||
content.append(input, node("small", tile.entity));
|
swatches.append(button);
|
||||||
const actions = node("div", undefined, "tile-actions");
|
}
|
||||||
for (const [label, title, action, disabled] of [
|
palette.append(swatches);
|
||||||
["↑", "Omhoog", () => move(i, i - 1), i === 0],
|
body.append(palette);
|
||||||
["↓", "Omlaag", () => move(i, i + 1), i === layout.tiles.length - 1],
|
const foot = node("div", undefined, "sheet-foot");
|
||||||
[
|
const remove = node("button", "Verwijderen", "quiet danger");
|
||||||
"✕",
|
remove.type = "button";
|
||||||
"Verwijderen",
|
remove.onclick = () => removeTile(sheetIndex);
|
||||||
() => {
|
foot.append(remove);
|
||||||
layout.tiles.splice(i, 1);
|
if (domain !== "screen") {
|
||||||
markDirty();
|
const inspectButton = node("button", "Inspecteer", "quiet");
|
||||||
renderTiles();
|
inspectButton.type = "button";
|
||||||
renderResults();
|
inspectButton.onclick = () => { const entity = tile.entity; closeTileSheet(); inspect(entity); };
|
||||||
},
|
foot.append(inspectButton);
|
||||||
false,
|
}
|
||||||
],
|
const done = node("button", "Klaar");
|
||||||
]) {
|
done.type = "button";
|
||||||
const b = node("button", label);
|
done.onclick = closeTileSheet;
|
||||||
b.title = title;
|
foot.append(done);
|
||||||
b.setAttribute("aria-label", title);
|
sheet.append(head, body, foot);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
function addTile(id, at) {
|
function addTile(id, at) {
|
||||||
if (layout.tiles.some((t) => t.entity === id) || layout.tiles.length >= tileLimit()) return;
|
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 };
|
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) {
|
function enableDrag(element, source) {
|
||||||
element.addEventListener("pointerdown", (e) => {
|
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 });
|
Object.assign(drag, { source, element, start: { x: e.clientX, y: e.clientY }, pointerId: e.pointerId, active: false });
|
||||||
clearTimeout(drag.timer);
|
clearTimeout(drag.timer);
|
||||||
if (e.pointerType === "touch") drag.timer = setTimeout(() => beginDrag(e), 260);
|
if (e.pointerType === "touch") drag.timer = setTimeout(() => beginDrag(e), 260);
|
||||||
|
|||||||
@@ -112,12 +112,12 @@
|
|||||||
<span id="count">0 / 10</span>
|
<span id="count">0 / 10</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">
|
<p class="hint">
|
||||||
Klik op een tegel om bediening, weergave en breedte te kiezen.
|
Klik op een tegel voor naam, weergave, breedte en kleur; het
|
||||||
Sleep om te ordenen. Zes vakjes per pagina; een dubbelbrede
|
kruisje verwijdert. Sleep om te ordenen of sleep entiteiten uit
|
||||||
tegel telt voor twee en begint altijd links.
|
de lijst hierheen. Zes vakjes per pagina; een dubbelbrede tegel
|
||||||
|
telt voor twee en begint altijd links.
|
||||||
</p>
|
</p>
|
||||||
<div id="layout-preview" aria-label="Schermindeling"></div>
|
<div id="layout-preview" aria-label="Schermindeling"></div>
|
||||||
<ol id="tiles"></ol>
|
|
||||||
<p id="no-tiles" class="hint">
|
<p id="no-tiles" class="hint">
|
||||||
Voeg je eerste lamp, scène of apparaat toe via de kiezer.
|
Voeg je eerste lamp, scène of apparaat toe via de kiezer.
|
||||||
</p>
|
</p>
|
||||||
@@ -351,6 +351,7 @@
|
|||||||
<p id="firmware-status" role="status"></p>
|
<p id="firmware-status" role="status"></p>
|
||||||
<pre id="firmware-log"></pre>
|
<pre id="firmware-log"></pre>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
<dialog id="tile-sheet" class="tile-sheet" aria-label="Tegel instellen"></dialog>
|
||||||
<div id="toast" role="status" hidden></div>
|
<div id="toast" role="status" hidden></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -544,6 +544,9 @@ dialog label {
|
|||||||
bottom: 24px;
|
bottom: 24px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
padding: 14px 22px;
|
padding: 14px 22px;
|
||||||
background: var(--ink);
|
background: var(--ink);
|
||||||
color: white;
|
color: white;
|
||||||
@@ -842,3 +845,29 @@ pre {
|
|||||||
.palette-sample { width: 36px; height: 28px; border-radius: 7px; border: 1px solid #0002; }
|
.palette-sample { width: 36px; height: 28px; border-radius: 7px; border: 1px solid #0002; }
|
||||||
.palette-auto { background: linear-gradient(135deg, #fff 50%, #e5e7eb 50%); }
|
.palette-auto { background: linear-gradient(135deg, #fff 50%, #e5e7eb 50%); }
|
||||||
.tile-palette small { color: #52616e; }
|
.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; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: ESP Screen Manager
|
name: ESP Screen Manager
|
||||||
version: "0.2.13"
|
version: "0.2.14"
|
||||||
slug: esp_screen_manager
|
slug: esp_screen_manager
|
||||||
description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen.
|
description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen.
|
||||||
url: https://github.com/MaxGramser/homeassistant_espscreen
|
url: https://github.com/MaxGramser/homeassistant_espscreen
|
||||||
|
|||||||
Reference in New Issue
Block a user