Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 0.2.0b3

Restart Home Assistant after updating, then reload the browser or app once so the card loads anew.

### Fixed

- Four scene buttons beside the control stood in a 2×2 block, mirrored. They now stack in one column like the other buttons.
- The setup page showed plain text fields for icons when Home Assistant had not loaded its icon picker yet. The setup page and the card editor now load the picker themselves and show it as soon as it is there.
- Settings of earlier versions that no longer exist, such as the light glow, are dropped from a card's configuration when it is saved.

### Changed

- Buttons beside the control are stacked with plain CSS instead of per-button ordering, which is lighter on slow devices.

## 0.2.0b2

Restart Home Assistant after updating, then reload the browser or app once so the card loads anew.
Expand Down
28 changes: 12 additions & 16 deletions custom_components/cloudless_crescendo/frontend/crescendo-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const {
langOf,
lightA,
lightColor,
loadIconPicker,
lumOf,
rampColor,
rgbHue,
Expand Down Expand Up @@ -51,8 +52,6 @@ const AUTO_POS = { fader: "right", scenes: "top" };
const DEFAULTS = {
design: "gradient",
buttons_on: true,
show_value: false,
show_duration: false,
button_position: "auto",
slider: "wide",
color: "stripes",
Expand All @@ -66,7 +65,6 @@ const DEFAULTS = {
button_names: true,
details: "button",
colors_always: false,
tap_duration: 1,
show_title: true,
show_header_value: true,
power: "header",
Expand All @@ -76,6 +74,9 @@ const DEFAULTS = {
reset_place: "slider",
};

// Settings of earlier versions that no longer do anything; they are dropped from a card's config when it is saved.
const RETIRED = ["glow", "show_duration", "show_value", "tap_duration"];

// Below this, a slider or a column switches off instead of hanging at 1 or 2 %.
const OFF_SNAP = 3;

Expand Down Expand Up @@ -151,13 +152,13 @@ button { font: inherit; color: inherit; cursor: pointer; }
[data-pos="left"] .body { grid-template-columns: auto minmax(0, 1fr); }
[data-pos="left"] .btns { order: -1; }
[data-pos="right"] .body { grid-template-columns: minmax(0, 1fr) auto; }
[data-pos="left"] .presets, [data-pos="right"] .presets, [data-pos="left"] .scenes, [data-pos="right"] .scenes { grid-template-columns: minmax(78px, 112px); }
.btns { min-width: 0; align-self: start; }
/* Beside the control, neither side leaves a gap: the buttons grow to the control's height, and columns, a fader or channels grow to the buttons'. */
:is([data-pos="left"], [data-pos="right"]) .btns { align-self: stretch; display: grid; }
:is([data-pos="left"], [data-pos="right"]) :is(.presets, .scenes) { grid-auto-rows: 1fr; }
/* Beside the control the buttons stand like a fader: the last one at the top, the power button at the bottom. */
:is([data-pos="left"], [data-pos="right"]) :is(.presets, .scenes) > * { order: calc(1000 - var(--o, 0)); }
/* Beside the control the buttons stand like a fader: the last one at the top, the power button at the bottom.
The markup keeps its order, so the keyboard still moves from the power button up through the buttons. */
:is([data-pos="left"], [data-pos="right"]) :is(.presets, .scenes) { display: flex; flex-direction: column-reverse; min-width: 78px; max-width: 112px; }
:is([data-pos="left"], [data-pos="right"]) :is(.presets, .scenes) > * { flex: 1 1 0; min-width: 0; }
.reg:has(> .bars), .reg:has(> .fader-row), .reg:has(> .mix) { grid-template-rows: 1fr; }
.presets { display: grid; grid-template-columns: repeat(auto-fit, minmax(70px, 1fr)); gap: 8px; }
.preset { border: 1px solid var(--cr-line); background: transparent; border-radius: 10px; padding: 9px 8px; display: grid; gap: 1px; text-align: center; justify-items: center; min-height: 40px; align-content: center; }
Expand Down Expand Up @@ -284,9 +285,9 @@ button { font: inherit; color: inherit; cursor: pointer; }
@container (min-width: 470px) {
/* Matching the design: on a wide card the buttons stand beside the control, on a narrow one below it. */
[data-pos="auto"] .body { grid-template-columns: minmax(0, 1fr) minmax(120px, 170px); }
[data-pos="auto"] .presets, [data-pos="auto"] .scenes { grid-template-columns: minmax(0, 1fr); grid-auto-rows: 1fr; }
[data-pos="auto"] :is(.presets, .scenes) { display: flex; flex-direction: column-reverse; }
[data-pos="auto"] :is(.presets, .scenes) > * { flex: 1 1 0; min-width: 0; }
[data-pos="auto"] .btns { align-self: stretch; display: grid; }
[data-pos="auto"] :is(.presets, .scenes) > * { order: calc(1000 - var(--o, 0)); }
.fader { min-height: 340px; }
.bars { grid-template-rows: minmax(210px, 1fr) auto; }
.mix { grid-template-rows: auto minmax(230px, 1fr) auto; }
Expand Down Expand Up @@ -748,9 +749,6 @@ class CrescendoCard extends HTMLElement {
el.classList.toggle("four", list.length === 4 && !tile);
el.innerHTML = tile + list.map((b) => this._buttonHtml(b, sub, us, override, dark, true)).join("");
});
$$(".js-presets, .js-scenes", (el) =>
[...el.children].forEach((b, i) => b.style.setProperty("--o", b.classList.contains("power-tile") ? -1 : i)),
);
$$(".js-lanes", (el) => {
el.hidden = !cfg.show_lanes || !us.length;
el.innerHTML =
Expand Down Expand Up @@ -2009,6 +2007,7 @@ class CrescendoCardEditor extends HTMLElement {
// An icon picker reports while it is used; rebuilding the form then would lose the choice, so it stays as it is.
_set(patch, render = true) {
this._config = { ...this._config, ...patch };
for (const k of RETIRED) delete this._config[k];
const out = { ...this._config };
for (const [k, v] of Object.entries(DEFAULTS)) if (out[k] === v) delete out[k];
for (const [k, v] of Object.entries(out)) if (v === undefined) delete out[k];
Expand Down Expand Up @@ -2045,7 +2044,7 @@ class CrescendoCardEditor extends HTMLElement {
// Home Assistant loads its icon picker on demand; the form appears again once it is there.
if (!customElements.get("ha-icon-picker") && !this._waitingForPicker) {
this._waitingForPicker = true;
customElements.whenDefined("ha-icon-picker").then(() => this._render());
loadIconPicker().then((ready) => ready && this._render());
}
const iconField = (b, i) =>
customElements.get("ha-icon-picker")
Expand Down Expand Up @@ -2276,7 +2275,6 @@ class CrescendoCardEditor extends HTMLElement {
// Home Assistant's icon picker reports its choice only on itself, without bubbling, so each one gets its own listener.
this.shadowRoot.querySelectorAll("ha-icon-picker").forEach((el) => {
el.hass = this._hass;
el.value = el.getAttribute("value") || "";
el.addEventListener("value-changed", (e) => this._onChange(e));
});
}
Expand Down Expand Up @@ -2330,8 +2328,6 @@ class CrescendoCardEditor extends HTMLElement {
this._set({ [t.id]: t.value });
break;
case "buttons_on":
case "show_value":
case "show_duration":
case "group":
case "show_lanes":
case "columns_operable":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const {
roomLight,
seriesVar,
tr,
loadIconPicker,
units,
} = shared;

Expand Down Expand Up @@ -787,9 +788,13 @@ class CrescendoPanel extends HTMLElement {
// Home Assistant's icon picker reports its choice only on itself, without bubbling, so each one gets its own listener.
this.shadowRoot.querySelectorAll("#btnTable ha-icon-picker").forEach((el) => {
el.hass = this._hass;
el.value = el.getAttribute("value") || "";
el.addEventListener("value-changed", (e) => this._onChange(e));
});
// Home Assistant loads its icon picker on demand; the table appears again once it is there.
if (!customElements.get("ha-icon-picker") && !this._waitingForPicker) {
this._waitingForPicker = true;
loadIconPicker().then((ready) => ready && this._o && this.shadowRoot.getElementById("btnTable") && this._renderButtons());
}
}

_renderSettings() {
Expand Down
15 changes: 15 additions & 0 deletions custom_components/cloudless_crescendo/frontend/crescendo-shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,21 @@ export function stripeLayers(options, list, override, dark, vertical = false) {
export const friendly = (hass, entityId) => hass?.states?.[entityId]?.attributes?.friendly_name || entityId;

// Lights with the same mode and the same values at every stop count as one unit.
// Home Assistant loads its icon picker on demand. Opening the button card's editor pulls it in; then it is
// there for our own forms. Resolves once the picker is defined, however it arrived.
export async function loadIconPicker() {
if (customElements.get("ha-icon-picker")) return true;
try {
const helpers = await window.loadCardHelpers?.();
const card = helpers?.createCardElement({ type: "button", entity: "light.none" });
await card?.constructor?.getConfigElement?.();
} catch {
// Without the helpers the picker arrives whenever Home Assistant loads it for something else.
}
await customElements.whenDefined("ha-icon-picker");
return true;
}

export function units(options, hass, group, count = true) {
const list = options.lights.map((light, slot) => ({ light, slot, ids: [light.entity_id] }));
if (!group) return list.map((u) => ({ ...u, name: friendly(hass, u.light.entity_id) }));
Expand Down
2 changes: 1 addition & 1 deletion custom_components/cloudless_crescendo/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/MadisonMedia/cloudless-crescendo/issues",
"requirements": [],
"version": "0.2.0b2"
"version": "0.2.0b3"
}
8 changes: 8 additions & 0 deletions tests/frontend/harness.html
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,10 @@
const flat = await newCard({ button_position: "bottom", power: "buttons" });
const rowOrder = [...flat.shadowRoot.querySelectorAll(".js-presets > *")].sort((a, b) => a.getBoundingClientRect().left - b.getBoundingClientRect().left);
report("buttons below the control: power first, then from dim to bright", rowOrder[0].classList.contains("power-tile") && rowOrder.at(-1).dataset.pos === "100", rowOrder.map((b) => b.dataset.pos || "power").join(" "));
const fourScenes = await newCard({ design: "scenes", button_position: "right", buttons: [25, 50, 75, 100].map((p) => ({ name: `S${p}`, position: p, duration: 1 })) });
const sceneLefts = new Set([...fourScenes.shadowRoot.querySelectorAll(".js-scenes .preset")].map((b) => Math.round(b.getBoundingClientRect().left)));
const sceneTops = [...fourScenes.shadowRoot.querySelectorAll(".js-scenes .preset")].map((b) => b.getBoundingClientRect().top);
report("four scenes beside the control stand in one column, brightest on top", sceneLefts.size === 1 && sceneTops[3] < sceneTops[0], `${sceneLefts.size} columns`);
// A button takes its share of the time for 0 to 100 %: the object's (here 1 s) or the card's own.
const sped = await newCard({}, atPosition(62));
calls.length = 0;
Expand Down Expand Up @@ -586,6 +590,10 @@
await wait(20);
report("no glow setting", !ed("#glow"));
report("a picked icon is saved and the picker stays", editor._config.buttons?.[1]?.icon === "mdi:sofa" && ed('ha-icon-picker[data-i="1"]') === fakePicker, JSON.stringify(editor._config.buttons?.[1]));
editor.setConfig({ ...editor._config, glow: false });
ed("#snap").click();
await wait(30);
report("retired settings are dropped when the card is saved", !("glow" in editor._config));
const pick = async (design) => { ed(`[data-design="${design}"]`).click(); await wait(30); };
await pick("dial");
report("no horizontal slider setting for the dial", !ed("#slider") && !!ed("#dial_wedge"));
Expand Down
Loading