Dashboard Studio 2.4.1: compact litter box card
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## 2.4.1
|
||||
|
||||
- **Litter box, more compact:** the name gets the top line, the status sits beside **Clean** and the auto-clean switch, and today's figures are one short line (3 visits · 7 min · 4.6 kg), so nothing is cut off. One row high it all fits on a single line; tall cards keep the ring. On narrow cards the auto-clean switch makes way for **Clean**.
|
||||
|
||||
## 2.4.0
|
||||
|
||||
- **Litter box:** the Appliance card has a new kind, **Litter box**, for self-cleaning litter boxes such as the ZEDAR K1200 on Tuya. It shows what the box is doing in words (Ready, Cleaning…, Emptying, Levelling, Sanitizing, Sleeping), today's visits, the time in the box today and your cat's weight, with a **Clean now** button and the auto-clean switch. A full waste bin or a fault shows in amber. Choose the box's *Status* sensor and the rest of the device fills in by itself. A litter box card never hides itself.
|
||||
|
||||
@@ -236,7 +236,7 @@ By default the card only shows while the appliance is running or has just finish
|
||||
|
||||
#### Litter box
|
||||
|
||||
Set **Appliance** to **Litter box** for a self-cleaning litter box. Instead of a progress ring, the card says what the box is doing: *Ready*, *Cleaning…*, *Emptying*, *Levelling*, *Sanitizing* or *Sleeping*. Under that are today's visits, the time in the box today and your cat's weight. **Clean now** starts a cleaning cycle and the switch at the top turns auto clean on or off. The card never hides itself.
|
||||
Set **Appliance** to **Litter box** for a self-cleaning litter box. The card is compact: it says what the box is doing (*Ready*, *Cleaning…*, *Emptying*, *Levelling*, *Sanitizing* or *Sleeping*) with **Clean** and the **Auto** clean switch beside it, and today's visits, the time in the box and your cat's weight in one line below. It fits in one row high, where it all goes on one line; tall cards add a ring on top. On narrow cards only **Clean** stays. The card never hides itself.
|
||||
|
||||
**Tuya** (for example the ZEDAR K1200): choose the box's *Status* sensor and the card fills in the rest of the same device: *Excretion times (day)* as the visits, *Excretion duration* as the time in the box, *Cat weight*, the *Manual clean* button and the *Auto clean* switch. Sensors for a full waste bin or a fault become alerts, shown in amber. Not every box offers every sensor, and names can differ; you can always pick the entities by hand. Other brands work too, as long as there is a status sensor.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Dashboard Studio
|
||||
version: "2.4.0"
|
||||
version: "2.4.1"
|
||||
slug: dashboard_studio
|
||||
description: Drag-and-drop dashboard builder with a free-form grid, live entity widgets and themes.
|
||||
url: https://git.melvanveen.nl/veenm/ha-dashboard-studio
|
||||
|
||||
@@ -302,7 +302,7 @@
|
||||
"Clean button": "Schoonmaakknop", "Auto clean switch": "Schakelaar automatisch schoonmaken", "Choose the litter box’s status entity in the inspector.": "Kies de status-entiteit van de kattenbak in de inspector.",
|
||||
"Emptying": "Aan het legen", "Levelling": "Aan het egaliseren", "Sanitizing": "Aan het ontsmetten", "Sleeping": "Slaapt", "Ready": "Klaar voor gebruik",
|
||||
"Bin full": "Afvalbak vol", "Check the litter box": "Kijk even bij de kattenbak", "Cat inside": "Kat in de bak", "Needs attention": "Heeft aandacht nodig",
|
||||
"visits today": "bezoekjes vandaag", "visits": "bezoekjes", "in the box today": "in de bak vandaag", "cat’s weight": "gewicht kat", "Clean now": "Nu schoonmaken", "Auto clean": "Automatisch schoonmaken",
|
||||
"visits today": "bezoekjes vandaag", "visits": "bezoekjes", "visit": "bezoekje", "Clean": "Schoonmaken", "Auto": "Auto", "in the box today": "in de bak vandaag", "cat’s weight": "gewicht kat", "Clean now": "Nu schoonmaken", "Auto clean": "Automatisch schoonmaken",
|
||||
"Starts later": "Start later", "Action needed": "Actie nodig", "Error": "Fout", "Stopping": "Stoppen", "Door locked": "Deur vergrendeld",
|
||||
"Ready at {0}": "Klaar om {0}", "Check the appliance": "Kijk even bij het apparaat", "Something went wrong": "Er ging iets mis", "Stop": "Stop",
|
||||
"Hide this card when none of its cards are showing": "Verberg deze kaart als geen van de kaarten erin zichtbaar is",
|
||||
|
||||
@@ -1575,31 +1575,46 @@ body.in-haven #btn-new { display: none; }
|
||||
.appl-fact { font-size: var(--c-fs-meta); font-weight: 500; }
|
||||
.appl-big { font-size: clamp(22px, 17cqh, 34px); }
|
||||
|
||||
/* litter box: what it's doing in words, today's figures, clean now and auto clean */
|
||||
.lb .appl-text { gap: 4px; }
|
||||
.lb .appl-main-text { flex-wrap: nowrap; overflow: hidden; }
|
||||
.lb .appl-big { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.lb.phase-cleaning .appl-big { color: var(--d-accent); }
|
||||
.lb.phase-attention .appl-big { color: var(--warn); }
|
||||
.lb.phase-cleaning .appl-ico { color: var(--d-accent); }
|
||||
.lb.phase-attention .appl-ico { color: var(--warn); }
|
||||
/* litter box: compact; icon, name and controls on top, what it's doing, today's figures in one line */
|
||||
.lb { display: flex; align-items: stretch; gap: 16px; height: 100%; min-width: 0; }
|
||||
.lb-ring { display: none; }
|
||||
.lb-ring > svg { transform: none; }
|
||||
.lb-spin { transform-origin: 50px 50px; animation: lb-spin 1.6s linear infinite; }
|
||||
@keyframes lb-spin { to { transform: rotate(360deg); } }
|
||||
@media (prefers-reduced-motion: reduce) { .lb-spin { animation: none; } }
|
||||
.lb-figs { display: flex; gap: 18px; min-width: 0; }
|
||||
.lb-fig { display: flex; flex-direction: column; min-width: 0; }
|
||||
.lb-fig b { font-size: var(--c-fs-mid); font-weight: 400; line-height: 1.15; white-space: nowrap; }
|
||||
.lb-s { display: none; }
|
||||
.lb-fig span { font-size: var(--c-fs-meta); color: var(--d-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.lb-clean { flex: none; display: inline-flex; align-items: center; gap: 6px; height: var(--c-ctl); padding: 0 14px; border-radius: 99px; background: var(--d-fill); font-size: var(--c-fs-body); font-weight: 600; }
|
||||
.lb-body { flex: 1; min-width: 0; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; grid-template-areas: "ico name name" "state state acts" "figs figs figs" "warn warn warn";
|
||||
align-content: space-between; align-items: center; column-gap: 8px; row-gap: 2px; }
|
||||
.lb-ico { grid-area: ico; display: flex; font-size: var(--c-ico); color: var(--d-text-2); }
|
||||
.lb-name { grid-area: name; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.lb-acts { grid-area: acts; display: flex; align-items: center; gap: 8px; }
|
||||
.lb-state { grid-area: state; min-width: 0; font-size: clamp(20px, 16cqh, 32px); font-weight: 400; letter-spacing: -0.03em; line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.lb-figs { grid-area: figs; min-width: 0; display: flex; flex-wrap: wrap; column-gap: 12px; font-size: var(--c-fs-meta); color: var(--d-text-2); }
|
||||
.lb-fig { white-space: nowrap; }
|
||||
.lb-fig b { font-size: var(--c-fs-body); font-weight: 500; color: var(--d-text); }
|
||||
.lb-warn { grid-area: warn; display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; min-width: 0; }
|
||||
.lb.phase-cleaning .lb-state, .lb.phase-cleaning .lb-ico, .lb.phase-cleaning .appl-ico { color: var(--d-accent); }
|
||||
.lb.phase-attention .lb-state, .lb.phase-attention .lb-ico, .lb.phase-attention .appl-ico { color: var(--warn); }
|
||||
.lb-clean { flex: none; display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: var(--c-ctl); min-width: var(--c-ctl); padding: 0 12px; border-radius: 99px; background: var(--d-fill); font-size: var(--c-fs-meta); font-weight: 600; }
|
||||
.lb-clean:disabled { opacity: .5; cursor: default; }
|
||||
@media (hover: hover) { .lb-clean:not(:disabled):hover { background: color-mix(in srgb, var(--d-text) 14%, transparent); } }
|
||||
.lb-auto { flex: none; display: inline-flex; align-items: center; gap: 8px; font-size: var(--c-fs-meta); color: var(--d-text-2); }
|
||||
.lb-fig .unit { font-size: var(--c-fs-meta); }
|
||||
@container (max-width: 240px) { .lb-fig:nth-child(3) { display: none; } }
|
||||
@container (max-width: 400px) { .lb-figs { gap: 12px; } .lb-auto-l { display: none; } .lb-clean span { display: none; } .lb-clean { width: var(--c-ctl); padding: 0; justify-content: center; } }
|
||||
@container (max-height: 180px) { .lb .appl-text { gap: 2px; } .lb-fig { flex-direction: row; align-items: baseline; gap: 5px; } .lb-fig b { font-size: var(--c-fs-body); } .lb-l { display: none; } .lb-s { display: inline; } }
|
||||
.lb-auto { flex: none; display: inline-flex; align-items: center; gap: 6px; font-size: var(--c-fs-meta); color: var(--d-text-2); }
|
||||
/* narrow: the controls as icons */
|
||||
@container (max-width: 320px) { .lb-clean { padding: 0; } .lb-clean span { display: none; } }
|
||||
/* narrow: clean now stays, auto clean (a setting you rarely change) goes; the tiniest cards keep just the state */
|
||||
@container (max-width: 260px) { .lb-auto-l { display: none; } .lb-acts:has(.lb-clean) .lb-auto { display: none; } .lb-figs { column-gap: 8px; } }
|
||||
@container (max-width: 150px) { .lb-acts { display: none; } .lb-ico { display: none; } .lb-body { column-gap: 0; } }
|
||||
/* one row high: everything on one line */
|
||||
@container (max-height: 110px) {
|
||||
.lb-body { grid-template-columns: auto minmax(0, max-content) minmax(0, 1fr) auto; grid-template-areas: "ico state figs acts"; align-content: center; column-gap: 12px; }
|
||||
.lb-name, .lb-warn { display: none; }
|
||||
.lb-state { font-size: var(--c-fs-mid); }
|
||||
.lb-figs { line-height: 22px; height: 22px; overflow: hidden; } /* figures that don't fit wrap out of sight, whole */
|
||||
}
|
||||
@container (max-height: 110px) and (max-width: 360px) {
|
||||
.lb-figs { display: none; }
|
||||
.lb-body { grid-template-columns: auto minmax(0, 1fr) auto; grid-template-areas: "ico state acts"; }
|
||||
}
|
||||
@container (max-height: 110px) and (max-width: 280px) { .lb-acts:has(.lb-clean) .lb-auto { display: none; } }
|
||||
|
||||
/* camera and photos: square label plates (above) */
|
||||
|
||||
@@ -2032,4 +2047,12 @@ button.fader-input { cursor: pointer; }
|
||||
.lt-colors { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; }
|
||||
.lt-colors .swatches { flex: 1; min-width: 0; margin: 0; justify-content: flex-start; }
|
||||
.lt-colors .swatch { width: 28px; height: 28px; }
|
||||
@container (min-height: 260px) { .lb-figs { justify-content: center; } .lb-fig { align-items: center; } .lb .appl-main { flex-direction: column; } }
|
||||
/* tall: a ring on top, the rest centred below */
|
||||
@container (min-height: 260px) {
|
||||
.lb { flex-direction: column; align-items: center; justify-content: center; text-align: center; }
|
||||
.lb-body { flex: none; width: 100%; grid-template-columns: minmax(0, 1fr); grid-template-areas: "name" "state" "figs" "acts" "warn"; justify-items: center; row-gap: 4px; }
|
||||
.lb-ico { display: none; }
|
||||
.lb-figs { justify-content: center; }
|
||||
.lb-acts { margin-top: 6px; }
|
||||
}
|
||||
@container (min-height: 260px) and (min-width: 180px) { .lb-ring { display: grid; height: min(50%, 170px); } }
|
||||
|
||||
@@ -1072,39 +1072,44 @@
|
||||
return { phase: "ready", word: /^(standby|idle|ready|on|off|none|)$/.test(s) ? t("Ready") : st(s), alerts };
|
||||
}
|
||||
|
||||
// Compact: icon, name and controls on top, what it's doing below, today's figures as one short line.
|
||||
// One row high it all goes on one line; tall cards put a ring on top (see the container queries in style.css).
|
||||
function litterBox(c, ha) {
|
||||
if (!c.status) return `<div class="empty">${t("Choose the litter box’s status entity in the inspector.")}</div>`;
|
||||
const { phase, word, alerts } = litterPhase(c, ha);
|
||||
// figures: visits and time today, and the cat's weight; the unit in grey
|
||||
const fig = (eid, lbl, fmt, short = "") => {
|
||||
const val = (eid) => {
|
||||
const s = ha.states[eid];
|
||||
if (!s || ["unknown", "unavailable"].includes(s.state)) return "";
|
||||
const n = Number(s.state), unit = s.attributes.unit_of_measurement || "";
|
||||
const v = fmt ? fmt(n, unit) : `${isFinite(n) ? n.toLocaleString(LOCALE, { maximumFractionDigits: 1 }) : esc(s.state)}${unit ? `<small class="unit"> ${esc(unit)}</small>` : ""}`;
|
||||
return `<div class="lb-fig"><b class="num">${v}</b><span class="lb-l">${esc(lbl)}</span>${short ? `<span class="lb-s">${esc(short)}</span>` : ""}</div>`;
|
||||
return s && !["unknown", "unavailable"].includes(s.state) ? s : null;
|
||||
};
|
||||
// time today: Tuya reports it in seconds or minutes, shown as minutes
|
||||
const mins = (n, unit) => {
|
||||
if (!isFinite(n)) return "–";
|
||||
const m = Math.round(/^s/.test(unit) ? n / 60 : /^h/.test(unit) ? n * 60 : n);
|
||||
return `${m}<small class="unit"> min</small>`;
|
||||
};
|
||||
const figs = [fig(c.visits, t("visits today"), (n) => (isFinite(n) ? String(Math.round(n)) : "–"), t("visits")), fig(c.timeToday, t("in the box today"), mins), fig(c.catWeight, t("cat’s weight"))].join("");
|
||||
const fig = (v, unit, title) => `<span class="lb-fig" title="${esc(title)}"><b class="num">${v}</b> ${esc(unit)}</span>`;
|
||||
const visits = val(c.visits), time = val(c.timeToday), weight = val(c.catWeight);
|
||||
const figs = [
|
||||
visits && isFinite(Number(visits.state)) && fig(Math.round(Number(visits.state)), Number(visits.state) === 1 ? t("visit") : t("visits"), t("Visits today")),
|
||||
// time today: Tuya reports it in seconds or minutes, shown as minutes
|
||||
time && isFinite(Number(time.state)) && (() => {
|
||||
const n = Number(time.state), unit = String(time.attributes.unit_of_measurement || "min").toLowerCase();
|
||||
return fig(Math.round(unit.startsWith("s") ? n / 60 : unit.startsWith("h") ? n * 60 : n), t("min"), t("Time in the box today"));
|
||||
})(),
|
||||
weight && fig(isFinite(Number(weight.state)) ? Number(weight.state).toLocaleString(LOCALE, { maximumFractionDigits: 1 }) : esc(weight.state), weight.attributes.unit_of_measurement || "", t("Cat’s weight")),
|
||||
].filter(Boolean).join("");
|
||||
const device = friendly(ha.states[c.status]).replace(/\s*(status|state)$/i, "");
|
||||
const strip = (txt) => (device && txt.toLowerCase().startsWith(device.toLowerCase() + " ") ? txt.slice(device.length + 1) : txt);
|
||||
const warn = alerts.map((e) => `<span class="appl-fact warn">${Icons.svg("shield")}<span>${esc(cap(strip(friendly(ha.states[e]))))}</span></span>`).join("");
|
||||
const clean = c.cleanBtn && ha.states[c.cleanBtn]
|
||||
? `<button class="lb-clean wbtn" data-act="svc" data-e="${esc(c.cleanBtn)}" data-v="${domainOf(c.cleanBtn) === "script" ? "script.turn_on" : "button.press"}" ${phase === "cleaning" ? "disabled" : ""}>${Icons.svg("sparkles")}<span>${t("Clean now")}</span></button>` : "";
|
||||
? `<button class="lb-clean wbtn" data-act="svc" data-e="${esc(c.cleanBtn)}" data-v="${domainOf(c.cleanBtn) === "script" ? "script.turn_on" : "button.press"}" ${phase === "cleaning" ? "disabled" : ""} title="${t("Clean now")}" aria-label="${t("Clean now")}">${Icons.svg("sparkles")}<span>${t("Clean")}</span></button>` : "";
|
||||
const auto = ha.states[c.autoClean];
|
||||
const autoSw = auto ? `<span class="lb-auto"><span class="lb-auto-l">${t("Auto clean")}</span><button class="light-switch wbtn" role="switch" aria-checked="${isOn(auto)}" data-act="toggle" data-e="${esc(c.autoClean)}" aria-label="${t("Auto clean")}"><span class="switch"></span></button></span>` : "";
|
||||
return `<div class="appl lb phase-${phase}">
|
||||
const autoSw = auto ? `<span class="lb-auto" title="${t("Auto clean")}"><span class="lb-auto-l">${t("Auto")}</span><button class="light-switch wbtn" role="switch" aria-checked="${isOn(auto)}" data-act="toggle" data-e="${esc(c.autoClean)}" aria-label="${t("Auto clean")}"><span class="switch"></span></button></span>` : "";
|
||||
const ico = phase === "attention" ? "shield" : "paw";
|
||||
return `<div class="lb phase-${phase}">
|
||||
<div class="appl-ring lb-ring" aria-hidden="true"><svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="44" class="track"/>${phase === "cleaning" ? `<circle cx="50" cy="50" r="44" class="fill lb-spin" stroke-dasharray="70 207"/>` : ""}</svg>
|
||||
<span class="appl-ico">${Icons.svg(phase === "attention" ? "shield" : "paw")}</span></div>
|
||||
<div class="appl-text">
|
||||
<div class="appl-top"><span class="name">${esc(c.name || t("Litter box"))}</span>${autoSw}</div>
|
||||
<div class="appl-main"><div class="appl-main-text"><span class="appl-big ${phase === "off" ? "appl-off" : ""}">${esc(word)}</span></div>${clean}</div>
|
||||
<span class="appl-ico">${Icons.svg(ico)}</span></div>
|
||||
<div class="lb-body">
|
||||
<span class="lb-ico" aria-hidden="true">${Icons.svg(ico)}</span>
|
||||
<span class="name lb-name">${esc(c.name || t("Litter box"))}</span>
|
||||
${clean || autoSw ? `<div class="lb-acts">${autoSw}${clean}</div>` : ""}
|
||||
<span class="lb-state ${phase === "off" ? "appl-off" : ""}">${esc(word)}</span>
|
||||
${figs ? `<div class="lb-figs">${figs}</div>` : ""}
|
||||
${warn ? `<div class="appl-facts">${warn}</div>` : ""}
|
||||
${warn ? `<div class="lb-warn">${warn}</div>` : ""}
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user