Dashboard Studio 1.8.0: vacuum card, clock position and background, light groups
- Vacuum card: status, battery, start/pause, dock, locate, suction power and an optional live map from a camera or image entity - Clock: 3x3 text position picker and a background image from links or Immich albums (shared slideshow code with the Photos card), dimmable - Light card lists the lights of a light group with their own switches; picking a group grows the card to fit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -625,6 +625,9 @@
|
||||
${list.map((e, i) => `<div class="ent-item"><input class="input" type="text" list="${lid}" ${data} data-idx="${i}" value="${esc(e)}" autocomplete="off" spellcheck="false" aria-label="${esc(f.l)} ${i + 1}"><button class="btn icon" data-ent-del="${i}" ${data} aria-label="${esc(tr("Remove {0}", e))}">${Icons.svg("close")}</button></div>`).join("")}
|
||||
<input class="input" type="text" list="${lid}" ${data} data-idx="new" placeholder="${tr("Add an entity…")}" autocomplete="off" spellcheck="false" aria-label="Add to ${esc(f.l)}"></div>`;
|
||||
}
|
||||
case "pos9": // a 3×3 grid for placing content in the card
|
||||
return `<div class="field"><span class="field-label" id="${id}">${esc(f.l)}</span><div class="pos9" role="group" aria-labelledby="${id}">
|
||||
${f.o.map((o) => `<button type="button" ${data} data-v="${o}" aria-pressed="${o === value}" aria-label="${esc(optLabel(o))}" title="${esc(optLabel(o))}"><i></i></button>`).join("")}</div></div>`;
|
||||
case "albums":
|
||||
return `<div class="field"><span class="field-label">${esc(f.l)}</span><div class="albums" ${data} data-selected="${esc(JSON.stringify(value || []))}"><div class="muted small">${tr("Loading albums…")}</div></div></div>`;
|
||||
default:
|
||||
@@ -874,6 +877,8 @@
|
||||
if (path === "cfg") {
|
||||
if (idx != null) w.cfg[k][Number(idx)] = v;
|
||||
else w.cfg[k] = v;
|
||||
// A light group lists its lights underneath, which needs room.
|
||||
if (w.type === "light" && k === "entity" && Array.isArray(ha.states[v]?.attributes.entity_id) && w.h && w.h < 4) { w.h = 4; resolveCollisions(w); }
|
||||
} else if (path === "style") { (w.style ||= {})[k] = v; }
|
||||
else if (path === "pos") {
|
||||
const n = Math.max(k === "w" || k === "h" ? 1 : 0, Math.round(Number(v) || 0));
|
||||
@@ -889,7 +894,7 @@
|
||||
} else if (path === "grid") { current.grid[k] = v === "" ? 0 : v; }
|
||||
else if (path === "kiosk") { current.kiosk[k] = v; }
|
||||
else if (path === "dash") { current.name = v; renderDashSelect(); }
|
||||
}, key, { rerender: ["pos", "theme", "grid"].includes(path) });
|
||||
}, key, { rerender: ["pos", "theme", "grid"].includes(path) || (path === "cfg" && k === "entity") });
|
||||
|
||||
if (path === "cfg" || path === "style") renderWidget(selectedWidget());
|
||||
const fieldType = w && Widgets[w.type].fields.find((f) => f.k === k)?.t;
|
||||
@@ -903,7 +908,7 @@
|
||||
if (albums) {
|
||||
if (e.type !== "change") return;
|
||||
const ids = [...albums.querySelectorAll("input:checked")].map((i) => i.value);
|
||||
applyField({ dataset: { path: "cfg", k: "albums" }, tagName: "DIV", classList: { add() {}, remove() {} } }, ids, false);
|
||||
applyField({ dataset: { path: "cfg", k: albums.dataset.k }, tagName: "DIV", classList: { add() {}, remove() {} } }, ids, false);
|
||||
return;
|
||||
}
|
||||
if (t.dataset.sync) { // color swatch -> text field
|
||||
@@ -1062,6 +1067,7 @@
|
||||
const t = Math.round(((s?.attributes.temperature ?? 20) + Number(a.dataset.v)) * 10) / 10;
|
||||
ha.callService("climate", "set_temperature", { temperature: t }, eid);
|
||||
} else if (act === "hvac") ha.callService("climate", "set_hvac_mode", { hvac_mode: a.dataset.v }, eid);
|
||||
else if (act === "vac-fan") ha.callService("vacuum", "set_fan_speed", { fan_speed: a.dataset.v }, eid);
|
||||
else if (act === "mp-select") {
|
||||
const box = a.closest(".w"), w = findWidget(box.dataset.id);
|
||||
caches[w.id].sel = a.dataset.v;
|
||||
@@ -1417,7 +1423,7 @@
|
||||
insp.addEventListener("input", onInspectorInput);
|
||||
insp.addEventListener("change", onInspectorInput);
|
||||
insp.addEventListener("click", (e) => {
|
||||
const seg = e.target.closest(".seg button[data-path]");
|
||||
const seg = e.target.closest(".seg button[data-path], .pos9 button[data-path]");
|
||||
if (seg) { applyField(seg, seg.dataset.v, true); return; }
|
||||
const p = e.target.closest("[data-preset]");
|
||||
if (p) {
|
||||
|
||||
Reference in New Issue
Block a user