Compare commits

..

5 Commits

Author SHA1 Message Date
Paul Bottein
bf78effe29 Refactor state function 2025-09-15 09:21:52 +02:00
Paul Bottein
6a51308ee3 Return computed color for state color instead of css variable 2025-09-12 18:53:05 +02:00
renovate[bot]
acab2d5ead Update dependency ua-parser-js to v2.0.5 (#27024)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-12 16:42:16 +02:00
Paul Bottein
046fc00f73 Add home assistant bottom sheet (#26948)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-09-12 15:33:30 +02:00
Marcin
05775c411b Add transition to button background (#27021) 2025-09-12 14:57:33 +02:00
59 changed files with 743 additions and 595 deletions

View File

@@ -135,7 +135,7 @@
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
"ua-parser-js": "2.0.4",
"ua-parser-js": "2.0.5",
"vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",

View File

@@ -1,15 +1,13 @@
export const batteryStateColorProperty = (
state: string
): string | undefined => {
export const batteryStateColor = (state: string): string | undefined => {
const value = Number(state);
if (isNaN(value)) {
return undefined;
}
if (value >= 70) {
return "--state-sensor-battery-high-color";
return "var(--state-sensor-battery-high-color)";
}
if (value >= 30) {
return "--state-sensor-battery-medium-color";
return "var(--state-sensor-battery-medium-color)";
}
return "--state-sensor-battery-low-color";
return "var(--state-sensor-battery-low-color)";
};

View File

@@ -5,12 +5,15 @@ import { computeDomain } from "./compute_domain";
export function stateActive(stateObj: HassEntity, state?: string): boolean {
const domain = computeDomain(stateObj.entity_id);
const compareState = state !== undefined ? state : stateObj?.state;
return domainStateActive(domain, compareState);
}
export function domainStateActive(domain: string, state: string) {
if (["button", "event", "input_button", "scene"].includes(domain)) {
return compareState !== UNAVAILABLE;
return state !== UNAVAILABLE;
}
if (isUnavailableState(compareState)) {
if (isUnavailableState(state)) {
return false;
}
@@ -18,40 +21,40 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
// such as "alert" where "off" is still a somewhat active state and
// therefore gets a custom color and "idle" is instead the state that
// matches what most other domains consider inactive.
if (compareState === OFF && domain !== "alert") {
if (state === OFF && domain !== "alert") {
return false;
}
// Custom cases
switch (domain) {
case "alarm_control_panel":
return compareState !== "disarmed";
return state !== "disarmed";
case "alert":
// "on" and "off" are active, as "off" just means alert was acknowledged but is still active
return compareState !== "idle";
return state !== "idle";
case "cover":
return compareState !== "closed";
return state !== "closed";
case "device_tracker":
case "person":
return compareState !== "not_home";
return state !== "not_home";
case "lawn_mower":
return ["mowing", "error"].includes(compareState);
return ["mowing", "error"].includes(state);
case "lock":
return compareState !== "locked";
return state !== "locked";
case "media_player":
return compareState !== "standby";
return state !== "standby";
case "vacuum":
return !["idle", "docked", "paused"].includes(compareState);
return !["idle", "docked", "paused"].includes(state);
case "valve":
return compareState !== "closed";
return state !== "closed";
case "plant":
return compareState === "problem";
return state === "problem";
case "group":
return ["on", "home", "open", "locked", "problem"].includes(compareState);
return ["on", "home", "open", "locked", "problem"].includes(state);
case "timer":
return compareState === "active";
return state === "active";
case "camera":
return compareState === "streaming";
return state === "streaming";
}
return true;

View File

@@ -1,13 +1,11 @@
/** Return a color representing a state. */
import type { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE } from "../../data/entity";
import type { GroupEntity } from "../../data/group";
import { computeGroupDomain } from "../../data/group";
import { computeCssVariable } from "../../resources/css-variables";
import { slugify } from "../string/slugify";
import { batteryStateColorProperty } from "./color/battery_color";
import { batteryStateColor } from "./color/battery_color";
import { computeDomain } from "./compute_domain";
import { stateActive } from "./state_active";
import { domainStateActive } from "./state_active";
const STATE_COLORED_DOMAIN = new Set([
"alarm_control_panel",
@@ -42,73 +40,20 @@ const STATE_COLORED_DOMAIN = new Set([
"water_heater",
]);
export const stateColorCss = (stateObj: HassEntity, state?: string) => {
const compareState = state !== undefined ? state : stateObj?.state;
if (compareState === UNAVAILABLE) {
return `var(--state-unavailable-color)`;
}
const properties = stateColorProperties(stateObj, state);
if (properties) {
return computeCssVariable(properties);
}
return undefined;
};
export const domainStateColorProperties = (
domain: string,
export const stateColor = (
element: HTMLElement | CSSStyleDeclaration,
stateObj: HassEntity,
state?: string
): string[] => {
const compareState = state !== undefined ? state : stateObj.state;
const active = stateActive(stateObj, state);
return domainColorProperties(
domain,
stateObj.attributes.device_class,
compareState,
active
);
};
export const domainColorProperties = (
domain: string,
deviceClass: string | undefined,
state: string,
active: boolean
) => {
const properties: string[] = [];
const stateKey = slugify(state, "_");
const activeKey = active ? "active" : "inactive";
if (deviceClass) {
properties.push(`--state-${domain}-${deviceClass}-${stateKey}-color`);
}
properties.push(
`--state-${domain}-${stateKey}-color`,
`--state-${domain}-${activeKey}-color`,
`--state-${activeKey}-color`
);
return properties;
};
export const stateColorProperties = (
stateObj: HassEntity,
state?: string
): string[] | undefined => {
const compareState = state !== undefined ? state : stateObj?.state;
const domain = computeDomain(stateObj.entity_id);
const dc = stateObj.attributes.device_class;
const compareState = state !== undefined ? state : stateObj.state;
// Special rules for battery coloring
if (domain === "sensor" && dc === "battery") {
const property = batteryStateColorProperty(compareState);
const property = batteryStateColor(compareState);
if (property) {
return [property];
return property;
}
}
@@ -116,17 +61,52 @@ export const stateColorProperties = (
if (domain === "group") {
const groupDomain = computeGroupDomain(stateObj as GroupEntity);
if (groupDomain && STATE_COLORED_DOMAIN.has(groupDomain)) {
return domainStateColorProperties(groupDomain, stateObj, state);
return domainStateColor(element, groupDomain, undefined, compareState);
}
}
if (STATE_COLORED_DOMAIN.has(domain)) {
return domainStateColorProperties(domain, stateObj, state);
return domainStateColor(element, domain, dc, compareState);
}
return undefined;
};
export const domainStateColor = (
element: HTMLElement | CSSStyleDeclaration,
domain: string,
deviceClass: string | undefined,
state: string
) => {
const style =
element instanceof CSSStyleDeclaration
? element
: getComputedStyle(element);
const stateKey = slugify(state, "_");
const active = domainStateActive(domain, state);
const activeKey = active ? "active" : "inactive";
const variables = [
`--state-${domain}-${stateKey}-color`,
`--state-${domain}-${activeKey}-color`,
`--state-${activeKey}-color`,
];
if (deviceClass) {
variables.unshift(`--state-${domain}-${deviceClass}-${stateKey}-color`);
}
for (const variable of variables) {
const value = style.getPropertyValue(variable).trim();
if (value) {
return value;
}
}
return undefined;
};
export const stateColorBrightness = (stateObj: HassEntity): string => {
if (
stateObj.attributes.brightness &&

View File

@@ -3,7 +3,7 @@ import { getGraphColorByIndex } from "../../common/color/colors";
import { hex2rgb, lab2hex, rgb2lab } from "../../common/color/convert-color";
import { labBrighten } from "../../common/color/lab";
import { computeDomain } from "../../common/entity/compute_domain";
import { stateColorProperties } from "../../common/entity/state_color";
import { stateColor } from "../../common/entity/state_color";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { computeCssValue } from "../../resources/css-variables";
@@ -30,22 +30,16 @@ function computeTimelineStateColor(
return computeCssValue("--history-unknown-color", computedStyles);
}
const properties = stateColorProperties(stateObj, state);
const color = stateColor(computedStyles, stateObj, state);
if (!properties) {
return undefined;
}
const rgb = computeCssValue(properties, computedStyles);
if (!rgb) return undefined;
if (!color) return undefined;
const domain = computeDomain(stateObj.entity_id);
const shade = DOMAIN_STATE_SHADES[domain]?.[state] as number | number;
if (!shade) {
return rgb;
return color;
}
return lab2hex(labBrighten(rgb2lab(hex2rgb(rgb)), shade));
return lab2hex(labBrighten(rgb2lab(hex2rgb(color)), shade));
}
let colorIndex = 0;

View File

@@ -9,7 +9,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import {
stateColorBrightness,
stateColorCss,
stateColor,
} from "../../common/entity/state_color";
import { iconColorCSS } from "../../common/style/icon_color_css";
import { cameraUrlWithWidthHeight } from "../../data/camera";
@@ -148,7 +148,7 @@ export class StateBadge extends LitElement {
// Externally provided overriding color wins over state color
iconStyle.color = this.color;
} else if (this._stateColor) {
const color = stateColorCss(stateObj);
const color = stateColor(this, stateObj);
if (color) {
iconStyle.color = color;
}
@@ -169,7 +169,8 @@ export class StateBadge extends LitElement {
if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action;
if (hvacAction in CLIMATE_HVAC_ACTION_TO_MODE) {
iconStyle.color = stateColorCss(
iconStyle.color = stateColor(
this,
stateObj,
CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]
)!;

View File

@@ -1,262 +1,62 @@
import { css, html, LitElement } from "lit";
import { customElement, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import { css, html, LitElement, type PropertyValues } from "lit";
import "@home-assistant/webawesome/dist/components/drawer/drawer";
import { customElement, property, state } from "lit/decorators";
const ANIMATION_DURATION_MS = 300;
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
/**
* A bottom sheet component that slides up from the bottom of the screen.
*
* The bottom sheet provides a draggable interface that allows users to resize
* the sheet by dragging the handle at the top. It supports both mouse and touch
* interactions and automatically closes when dragged below a 20% of screen height.
*
* @fires bottom-sheet-closed - Fired when the bottom sheet is closed
*
* @cssprop --ha-bottom-sheet-border-width - Border width for the sheet
* @cssprop --ha-bottom-sheet-border-style - Border style for the sheet
* @cssprop --ha-bottom-sheet-border-color - Border color for the sheet
*/
@customElement("ha-bottom-sheet")
export class HaBottomSheet extends LitElement {
@query("dialog") private _dialog!: HTMLDialogElement;
@property({ type: Boolean }) public open = false;
private _dragging = false;
@state() private _drawerOpen = false;
private _dragStartY = 0;
private _handleAfterHide() {
this.open = false;
const ev = new Event("closed", {
bubbles: true,
composed: true,
});
this.dispatchEvent(ev);
}
private _initialSize = 0;
@state() private _dialogMaxViewpointHeight = 70;
@state() private _dialogMinViewpointHeight = 55;
@state() private _dialogViewportHeight?: number;
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("open")) {
this._drawerOpen = this.open;
}
}
render() {
return html`<dialog
open
@transitionend=${this._handleTransitionEnd}
style=${styleMap({
height: this._dialogViewportHeight
? `${this._dialogViewportHeight}vh`
: "auto",
maxHeight: `${this._dialogMaxViewpointHeight}vh`,
minHeight: `${this._dialogMinViewpointHeight}vh`,
})}
>
<div class="handle-wrapper">
<div
@mousedown=${this._handleMouseDown}
@touchstart=${this._handleTouchStart}
class="handle"
></div>
</div>
<slot></slot>
</dialog>`;
}
protected firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this._openSheet();
}
private _openSheet() {
requestAnimationFrame(() => {
// trigger opening animation
this._dialog.classList.add("show");
});
}
public closeSheet() {
requestAnimationFrame(() => {
this._dialog.classList.remove("show");
});
}
private _handleTransitionEnd() {
if (this._dialog.classList.contains("show")) {
// after show animation is done
// - set the height to the natural height, to prevent content shift when switch content
// - set max height to 90vh, so it opens at max 70vh but can be resized to 90vh
this._dialogViewportHeight =
(this._dialog.offsetHeight / window.innerHeight) * 100;
this._dialogMaxViewpointHeight = 90;
this._dialogMinViewpointHeight = 20;
} else {
// after close animation is done close dialog element and fire closed event
this._dialog.close();
fireEvent(this, "bottom-sheet-closed");
}
}
connectedCallback() {
super.connectedCallback();
// register event listeners for drag handling
document.addEventListener("mousemove", this._handleMouseMove);
document.addEventListener("mouseup", this._handleMouseUp);
document.addEventListener("touchmove", this._handleTouchMove, {
passive: false,
});
document.addEventListener("touchend", this._handleTouchEnd);
document.addEventListener("touchcancel", this._handleTouchEnd);
}
disconnectedCallback() {
super.disconnectedCallback();
// unregister event listeners for drag handling
document.removeEventListener("mousemove", this._handleMouseMove);
document.removeEventListener("mouseup", this._handleMouseUp);
document.removeEventListener("touchmove", this._handleTouchMove);
document.removeEventListener("touchend", this._handleTouchEnd);
document.removeEventListener("touchcancel", this._handleTouchEnd);
}
private _handleMouseDown = (ev: MouseEvent) => {
this._startDrag(ev.clientY);
};
private _handleTouchStart = (ev: TouchEvent) => {
// Prevent the browser from interpreting this as a scroll/PTR gesture.
ev.preventDefault();
this._startDrag(ev.touches[0].clientY);
};
private _startDrag(clientY: number) {
this._dragging = true;
this._dragStartY = clientY;
this._initialSize = (this._dialog.offsetHeight / window.innerHeight) * 100;
document.body.style.setProperty("cursor", "grabbing");
}
private _handleMouseMove = (ev: MouseEvent) => {
if (!this._dragging) {
return;
}
this._updateSize(ev.clientY);
};
private _handleTouchMove = (ev: TouchEvent) => {
if (!this._dragging) {
return;
}
ev.preventDefault(); // Prevent scrolling
this._updateSize(ev.touches[0].clientY);
};
private _updateSize(clientY: number) {
const deltaY = this._dragStartY - clientY;
const viewportHeight = window.innerHeight;
const deltaVh = (deltaY / viewportHeight) * 100;
// Calculate new size and clamp between 10vh and 90vh
let newSize = this._initialSize + deltaVh;
newSize = Math.max(10, Math.min(90, newSize));
// on drag down and below 20vh
if (newSize < 20 && deltaY < 0) {
this._endDrag();
this.closeSheet();
return;
}
this._dialogViewportHeight = newSize;
}
private _handleMouseUp = () => {
this._endDrag();
};
private _handleTouchEnd = () => {
this._endDrag();
};
private _endDrag() {
if (!this._dragging) {
return;
}
this._dragging = false;
document.body.style.removeProperty("cursor");
return html`
<wa-drawer
placement="bottom"
.open=${this._drawerOpen}
@wa-after-hide=${this._handleAfterHide}
without-header
>
<slot></slot>
</wa-drawer>
`;
}
static styles = css`
.handle-wrapper {
position: absolute;
top: 0;
width: 100%;
padding-bottom: 2px;
display: flex;
justify-content: center;
align-items: center;
cursor: grab;
touch-action: none;
}
.handle-wrapper .handle {
height: 20px;
width: 200px;
display: flex;
justify-content: center;
align-items: center;
z-index: 7;
padding-bottom: 76px;
}
.handle-wrapper .handle::after {
content: "";
border-radius: 8px;
height: 4px;
background: var(--divider-color, #e0e0e0);
width: 80px;
}
.handle-wrapper .handle:active::after {
cursor: grabbing;
}
dialog {
height: auto;
max-height: 70vh;
min-height: 30vh;
background-color: var(
wa-drawer {
--wa-color-surface-raised: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
display: flex;
flex-direction: column;
top: 0;
inset-inline-start: 0;
position: fixed;
width: calc(100% - 4px);
max-width: 100%;
border: none;
box-shadow: var(--wa-shadow-l);
padding: 0;
margin: 0;
top: auto;
inset-inline-end: auto;
bottom: 0;
inset-inline-start: 0;
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
border-top-left-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-2xl)
);
border-top-right-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-2xl)
);
transform: translateY(100%);
transition: transform ${ANIMATION_DURATION_MS}ms ease;
border-top-width: var(--ha-bottom-sheet-border-width);
border-right-width: var(--ha-bottom-sheet-border-width);
border-left-width: var(--ha-bottom-sheet-border-width);
border-bottom-width: 0;
border-style: var(--ha-bottom-sheet-border-style);
border-color: var(--ha-bottom-sheet-border-color);
--spacing: 0;
--size: auto;
--show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
--hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
}
dialog.show {
transform: translateY(0);
wa-drawer::part(dialog) {
border-top-left-radius: var(--ha-border-radius-lg);
border-top-right-radius: var(--ha-border-radius-lg);
max-height: 90vh;
}
wa-drawer::part(body) {
padding-bottom: var(--safe-area-inset-bottom);
}
`;
}
@@ -265,8 +65,4 @@ declare global {
interface HTMLElementTagNameMap {
"ha-bottom-sheet": HaBottomSheet;
}
interface HASSDomEvents {
"bottom-sheet-closed": undefined;
}
}

View File

@@ -57,6 +57,8 @@ export class HaButton extends Button {
font-size: var(--ha-font-size-m);
line-height: 1;
transition: background-color 0.15s ease-in-out;
}
:host([size="small"]) .button {

View File

@@ -0,0 +1,271 @@
import { css, html, LitElement } from "lit";
import { customElement, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import { BOTTOM_SHEET_ANIMATION_DURATION_MS } from "./ha-bottom-sheet";
/**
* A bottom sheet component that slides up from the bottom of the screen.
*
* The bottom sheet provides a draggable interface that allows users to resize
* the sheet by dragging the handle at the top. It supports both mouse and touch
* interactions and automatically closes when dragged below a 20% of screen height.
*
* @fires bottom-sheet-closed - Fired when the bottom sheet is closed
*
* @cssprop --ha-bottom-sheet-border-width - Border width for the sheet
* @cssprop --ha-bottom-sheet-border-style - Border style for the sheet
* @cssprop --ha-bottom-sheet-border-color - Border color for the sheet
*/
@customElement("ha-resizable-bottom-sheet")
export class HaResizableBottomSheet extends LitElement {
@query("dialog") private _dialog!: HTMLDialogElement;
private _dragging = false;
private _dragStartY = 0;
private _initialSize = 0;
@state() private _dialogMaxViewpointHeight = 70;
@state() private _dialogMinViewpointHeight = 55;
@state() private _dialogViewportHeight?: number;
render() {
return html`<dialog
open
@transitionend=${this._handleTransitionEnd}
style=${styleMap({
height: this._dialogViewportHeight
? `${this._dialogViewportHeight}vh`
: "auto",
maxHeight: `${this._dialogMaxViewpointHeight}vh`,
minHeight: `${this._dialogMinViewpointHeight}vh`,
})}
>
<div class="handle-wrapper">
<div
@mousedown=${this._handleMouseDown}
@touchstart=${this._handleTouchStart}
class="handle"
></div>
</div>
<slot></slot>
</dialog>`;
}
protected firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this._openSheet();
}
private _openSheet() {
requestAnimationFrame(() => {
// trigger opening animation
this._dialog.classList.add("show");
});
}
public closeSheet() {
requestAnimationFrame(() => {
this._dialog.classList.remove("show");
});
}
private _handleTransitionEnd() {
if (this._dialog.classList.contains("show")) {
// after show animation is done
// - set the height to the natural height, to prevent content shift when switch content
// - set max height to 90vh, so it opens at max 70vh but can be resized to 90vh
this._dialogViewportHeight =
(this._dialog.offsetHeight / window.innerHeight) * 100;
this._dialogMaxViewpointHeight = 90;
this._dialogMinViewpointHeight = 20;
} else {
// after close animation is done close dialog element and fire closed event
this._dialog.close();
fireEvent(this, "bottom-sheet-closed");
}
}
connectedCallback() {
super.connectedCallback();
// register event listeners for drag handling
document.addEventListener("mousemove", this._handleMouseMove);
document.addEventListener("mouseup", this._handleMouseUp);
document.addEventListener("touchmove", this._handleTouchMove, {
passive: false,
});
document.addEventListener("touchend", this._handleTouchEnd);
document.addEventListener("touchcancel", this._handleTouchEnd);
}
disconnectedCallback() {
super.disconnectedCallback();
// unregister event listeners for drag handling
document.removeEventListener("mousemove", this._handleMouseMove);
document.removeEventListener("mouseup", this._handleMouseUp);
document.removeEventListener("touchmove", this._handleTouchMove);
document.removeEventListener("touchend", this._handleTouchEnd);
document.removeEventListener("touchcancel", this._handleTouchEnd);
}
private _handleMouseDown = (ev: MouseEvent) => {
this._startDrag(ev.clientY);
};
private _handleTouchStart = (ev: TouchEvent) => {
// Prevent the browser from interpreting this as a scroll/PTR gesture.
ev.preventDefault();
this._startDrag(ev.touches[0].clientY);
};
private _startDrag(clientY: number) {
this._dragging = true;
this._dragStartY = clientY;
this._initialSize = (this._dialog.offsetHeight / window.innerHeight) * 100;
document.body.style.setProperty("cursor", "grabbing");
}
private _handleMouseMove = (ev: MouseEvent) => {
if (!this._dragging) {
return;
}
this._updateSize(ev.clientY);
};
private _handleTouchMove = (ev: TouchEvent) => {
if (!this._dragging) {
return;
}
ev.preventDefault(); // Prevent scrolling
this._updateSize(ev.touches[0].clientY);
};
private _updateSize(clientY: number) {
const deltaY = this._dragStartY - clientY;
const viewportHeight = window.innerHeight;
const deltaVh = (deltaY / viewportHeight) * 100;
// Calculate new size and clamp between 10vh and 90vh
let newSize = this._initialSize + deltaVh;
newSize = Math.max(10, Math.min(90, newSize));
// on drag down and below 20vh
if (newSize < 20 && deltaY < 0) {
this._endDrag();
this.closeSheet();
return;
}
this._dialogViewportHeight = newSize;
}
private _handleMouseUp = () => {
this._endDrag();
};
private _handleTouchEnd = () => {
this._endDrag();
};
private _endDrag() {
if (!this._dragging) {
return;
}
this._dragging = false;
document.body.style.removeProperty("cursor");
}
static styles = css`
.handle-wrapper {
position: absolute;
top: 0;
width: 100%;
padding-bottom: 2px;
display: flex;
justify-content: center;
align-items: center;
cursor: grab;
touch-action: none;
}
.handle-wrapper .handle {
height: 20px;
width: 200px;
display: flex;
justify-content: center;
align-items: center;
z-index: 7;
padding-bottom: 76px;
}
.handle-wrapper .handle::after {
content: "";
border-radius: 8px;
height: 4px;
background: var(--divider-color, #e0e0e0);
width: 80px;
}
.handle-wrapper .handle:active::after {
cursor: grabbing;
}
dialog {
height: auto;
max-height: 70vh;
min-height: 30vh;
background-color: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
display: flex;
flex-direction: column;
top: 0;
inset-inline-start: 0;
position: fixed;
width: calc(100% - 4px);
max-width: 100%;
border: none;
box-shadow: var(--wa-shadow-l);
padding: 0;
margin: 0;
top: auto;
inset-inline-end: auto;
bottom: 0;
inset-inline-start: 0;
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
border-top-left-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-2xl)
);
border-top-right-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-2xl)
);
transform: translateY(100%);
transition: transform ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms ease;
border-top-width: var(--ha-bottom-sheet-border-width);
border-right-width: var(--ha-bottom-sheet-border-width);
border-left-width: var(--ha-bottom-sheet-border-width);
border-bottom-width: 0;
border-style: var(--ha-bottom-sheet-border-style);
border-color: var(--ha-bottom-sheet-border-color);
}
dialog.show {
transform: translateY(0);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-resizable-bottom-sheet": HaResizableBottomSheet;
}
interface HASSDomEvents {
"bottom-sheet-closed": undefined;
}
}

View File

@@ -1,6 +1,7 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-bottom-sheet";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-icon";
import "../../components/ha-md-list";
@@ -40,6 +41,54 @@ export class ListItemsDialog
return nothing;
}
const content = html`
<div class="container">
<ha-md-list>
${this._params.items.map(
(item) => html`
<ha-md-list-item
type="button"
@click=${this._itemClicked}
.item=${item}
>
${item.iconPath
? html`
<ha-svg-icon
.path=${item.iconPath}
slot="start"
class="item-icon"
></ha-svg-icon>
`
: item.icon
? html`
<ha-icon
icon=${item.icon}
slot="start"
class="item-icon"
></ha-icon>
`
: nothing}
<span class="headline">${item.label}</span>
${item.description
? html`
<span class="supporting-text">${item.description}</span>
`
: nothing}
</ha-md-list-item>
`
)}
</ha-md-list>
</div>
`;
if (this._params.mode === "bottom-sheet") {
return html`
<ha-bottom-sheet placement="bottom" open @closed=${this._dialogClosed}>
${content}
</ha-bottom-sheet>
`;
}
return html`
<ha-dialog
open
@@ -47,43 +96,7 @@ export class ListItemsDialog
@closed=${this._dialogClosed}
hideActions
>
<div class="container">
<ha-md-list>
${this._params.items.map(
(item) => html`
<ha-md-list-item
type="button"
@click=${this._itemClicked}
.item=${item}
>
${item.iconPath
? html`
<ha-svg-icon
.path=${item.iconPath}
slot="start"
class="item-icon"
></ha-svg-icon>
`
: item.icon
? html`
<ha-icon
icon=${item.icon}
slot="start"
class="item-icon"
></ha-icon>
`
: nothing}
<span class="headline">${item.label}</span>
${item.description
? html`
<span class="supporting-text">${item.description}</span>
`
: nothing}
</ha-md-list-item>
`
)}
</ha-md-list>
</div>
${content}
</ha-dialog>
`;
}

View File

@@ -11,6 +11,7 @@ interface ListItem {
export interface ListItemsDialogParams {
title?: string;
items: ListItem[];
mode?: "dialog" | "bottom-sheet";
}
export const showListItemsDialog = (

View File

@@ -10,7 +10,7 @@ import {
temperature2rgb,
} from "../../../../common/color/convert-light-color";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stateColorCss } from "../../../../common/entity/state_color";
import { stateColor } from "../../../../common/entity/state_color";
import { throttle } from "../../../../common/util/throttle";
import "../../../../components/ha-control-slider";
import { UNAVAILABLE } from "../../../../data/entity";
@@ -66,7 +66,7 @@ class LightColorTempPicker extends LitElement {
this.stateObj.attributes.max_color_temp_kelvin ?? DEFAULT_MAX_KELVIN;
const gradient = this._generateTemperatureGradient(minKelvin!, maxKelvin);
const color = stateColorCss(this.stateObj);
const color = stateColor(this, this.stateObj);
return html`
<ha-control-slider

View File

@@ -2,7 +2,7 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import "../../../components/ha-control-button";
import "../../../components/ha-state-icon";
import type { AlarmControlPanelEntity } from "../../../data/alarm_control_panel";
@@ -32,7 +32,7 @@ class MoreInfoAlarmControlPanel extends LitElement {
return nothing;
}
const color = stateColorCss(this.stateObj);
const color = stateColor(this, this.stateObj);
const style = {
"--icon-color": color,
};

View File

@@ -3,7 +3,7 @@ import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attributes";
import "../../../components/ha-control-button";
@@ -82,7 +82,7 @@ class MoreInfoLock extends LitElement {
const supportsOpen = supportsFeature(this.stateObj, LockEntityFeature.OPEN);
const color = stateColorCss(this.stateObj);
const color = stateColor(this, this.stateObj);
const style = {
"--state-color": color,
};

View File

@@ -1,7 +1,7 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import "../../../components/ha-bottom-sheet";
import type { HaBottomSheet } from "../../../components/ha-bottom-sheet";
import "../../../components/ha-resizable-bottom-sheet";
import type { HaResizableBottomSheet } from "../../../components/ha-resizable-bottom-sheet";
import {
isCondition,
isScriptField,
@@ -37,7 +37,8 @@ export default class HaAutomationSidebar extends LitElement {
@state() private _yamlMode = false;
@query("ha-bottom-sheet") private _bottomSheetElement?: HaBottomSheet;
@query("ha-resizable-bottom-sheet")
private _bottomSheetElement?: HaResizableBottomSheet;
private _renderContent() {
// get config type
@@ -147,9 +148,9 @@ export default class HaAutomationSidebar extends LitElement {
if (this.narrow) {
return html`
<ha-bottom-sheet @bottom-sheet-closed=${this._closeSidebar}>
<ha-resizable-bottom-sheet @bottom-sheet-closed=${this._closeSidebar}>
${this._renderContent()}
</ha-bottom-sheet>
</ha-resizable-bottom-sheet>
`;
}

View File

@@ -11,7 +11,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import "../../../components/ha-badge";
import "../../../components/ha-ripple";
import "../../../components/ha-state-icon";
@@ -132,7 +132,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
}
// Fallback to state color
return stateColorCss(stateObj);
return stateColor(this, stateObj);
}
);

View File

@@ -5,7 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-select";
@@ -143,7 +143,7 @@ class HuiAlarmModeCardFeature
return nothing;
}
const color = stateColorCss(this._stateObj);
const color = stateColor(this, this._stateObj);
const supportedModes = supportedAlarmModes(this._stateObj).reverse();

View File

@@ -11,14 +11,13 @@ import {
toggleGroupEntities,
} from "../../../common/entity/group_entities";
import { stateActive } from "../../../common/entity/state_active";
import { domainColorProperties } from "../../../common/entity/state_color";
import { domainStateColor } from "../../../common/entity/state_color";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-domain-icon";
import "../../../components/ha-svg-icon";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import { forwardHaptic } from "../../../data/haptics";
import { computeCssVariable } from "../../../resources/css-variables";
import type { HomeAssistant } from "../../../types";
import type { AreaCardFeatureContext } from "../cards/hui-area-card";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
@@ -258,14 +257,12 @@ class HuiAreaControlsCardFeature
? ensureArray(button.filter.device_class)[0]
: undefined;
const activeColor = computeCssVariable(
domainColorProperties(domain, deviceClass, groupState, true)
);
const color = domainStateColor(this, domain, deviceClass, groupState);
return html`
<ha-control-button
style=${styleMap({
"--active-color": activeColor,
"--active-color": color,
})}
.title=${label}
aria-label=${label}

View File

@@ -5,7 +5,7 @@ import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import "../../../components/ha-control-select";
import type { ControlSelectOption } from "../../../components/ha-control-select";
import "../../../components/ha-control-select-menu";
@@ -145,7 +145,7 @@ class HuiClimateHvacModesCardFeature
return null;
}
const color = stateColorCss(this._stateObj);
const color = stateColor(this, this._stateObj);
const ordererHvacModes = (this._stateObj.attributes.hvac_modes || [])
.concat()

View File

@@ -5,7 +5,7 @@ import { computeCssColor } from "../../../common/color/compute-color";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-slider";
import { CoverEntityFeature, type CoverEntity } from "../../../data/cover";
@@ -84,11 +84,11 @@ class HuiCoverPositionCardFeature
const value = Math.max(Math.round(percentage), 0);
const openColor = stateColorCss(this._stateObj, "open");
const openColor = stateColor(this, this._stateObj, "open");
const color = this.color
? computeCssColor(this.color)
: stateColorCss(this._stateObj);
: stateColor(this, this._stateObj);
const style = {
"--feature-color": color,

View File

@@ -4,7 +4,7 @@ import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { CoverEntity } from "../../../data/cover";
import { CoverEntityFeature } from "../../../data/cover";
@@ -84,11 +84,11 @@ class HuiCoverTiltPositionCardFeature
const value = Math.max(Math.round(percentage), 0);
const openColor = stateColorCss(this._stateObj, "open");
const openColor = stateColor(this, this._stateObj, "open");
const color = this.color
? computeCssColor(this.color)
: stateColorCss(this._stateObj);
: stateColor(this, this._stateObj);
const style = {
"--feature-color": color,

View File

@@ -4,7 +4,7 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import "../../../components/ha-control-select";
import type { ControlSelectOption } from "../../../components/ha-control-select";
import { UNAVAILABLE } from "../../../data/entity";
@@ -112,7 +112,7 @@ class HuiFanOscillateCardFeature
return null;
}
const color = stateColorCss(this._stateObj);
const color = stateColor(this, this._stateObj);
const yesNo = ["no", "yes"] as const;
const options = yesNo.map<ControlSelectOption>((oscillating) => ({

View File

@@ -4,7 +4,7 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import "../../../components/ha-control-select";
import type { ControlSelectOption } from "../../../components/ha-control-select";
import { UNAVAILABLE } from "../../../data/entity";
@@ -117,7 +117,7 @@ class HuiHumidifierToggleCardFeature
return null;
}
const color = stateColorCss(this._stateObj);
const color = stateColor(this, this._stateObj);
const options = ["off", "on"].map<ControlSelectOption>((entityState) => ({
value: entityState,

View File

@@ -5,7 +5,7 @@ import { styleMap } from "lit/directives/style-map";
import { UNIT_F } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-control-button-group";
@@ -192,7 +192,7 @@ class HuiTargetTemperatureCardFeature
return nothing;
}
const stateColor = stateColorCss(this._stateObj);
const color = stateColor(this, this._stateObj);
const digits = this._step.toString().split(".")?.[1]?.length ?? 0;
const options = {
@@ -221,7 +221,7 @@ class HuiTargetTemperatureCardFeature
"temperature"
)}
style=${styleMap({
"--control-number-buttons-focus-color": stateColor,
"--control-number-buttons-focus-color": color,
})}
.disabled=${this._stateObj!.state === UNAVAILABLE}
.locale=${this.hass.locale}

View File

@@ -14,7 +14,7 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-switch";
@@ -134,7 +134,7 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
}
const onColor = "var(--feature-color)";
const offColor = stateColorCss(this._stateObj, "off");
const offColor = stateColor(this, this._stateObj, "off");
const isOn = this._stateObj.state === "on";
const isOff = this._stateObj.state === "off";

View File

@@ -5,7 +5,7 @@ import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-svg-icon";
@@ -125,8 +125,8 @@ class HuiValveOpenCloseCardFeature
}
// Determine colors and active states for toggle-style UI
const openColor = stateColorCss(this._stateObj, "open");
const closedColor = stateColorCss(this._stateObj, "closed");
const openColor = stateColor(this, this._stateObj, "open");
const closedColor = stateColor(this, this._stateObj, "closed");
const openIcon = mdiValveOpen;
const closedIcon = mdiValveClosed;

View File

@@ -5,7 +5,7 @@ import { computeCssColor } from "../../../common/color/compute-color";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-slider";
import { ValveEntityFeature, type ValveEntity } from "../../../data/valve";
@@ -84,11 +84,11 @@ class HuiValvePositionCardFeature
const value = Math.max(Math.round(percentage), 0);
const openColor = stateColorCss(this._stateObj, "open");
const openColor = stateColor(this, this._stateObj, "open");
const color = this.color
? computeCssColor(this.color)
: stateColorCss(this._stateObj);
: stateColor(this, this._stateObj);
const style = {
"--feature-color": color,

View File

@@ -3,7 +3,7 @@ import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-select";
@@ -130,7 +130,7 @@ class HuiWaterHeaterOperationModeCardFeature
return null;
}
const color = stateColorCss(this._stateObj);
const color = stateColor(this, this._stateObj);
const orderedModes = (this._stateObj.attributes.operation_list || [])
.concat()

View File

@@ -7,7 +7,7 @@ import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/chips/ha-assist-chip";
import "../../../components/ha-button";
@@ -241,7 +241,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
<ha-assist-chip
filled
style=${styleMap({
"--alarm-state-color": stateColorCss(stateObj),
"--alarm-state-color": stateColor(this, stateObj),
})}
class=${classMap({ [stateObj.state]: true })}
@click=${this._handleMoreInfo}

View File

@@ -18,7 +18,7 @@ import { computeStateDomain } from "../../../common/entity/compute_state_domain"
import { computeStateName } from "../../../common/entity/compute_state_name";
import {
stateColorBrightness,
stateColorCss,
stateColor,
} from "../../../common/entity/state_color";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { iconColorCSS } from "../../../common/style/icon_color_css";
@@ -341,11 +341,15 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action;
if (hvacAction in CLIMATE_HVAC_ACTION_TO_MODE) {
return stateColorCss(stateObj, CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]);
return stateColor(
this,
stateObj,
CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]
);
}
return undefined;
}
const iconColor = stateColorCss(stateObj);
const iconColor = stateColor(this, stateObj);
if (iconColor) {
return iconColor;
}

View File

@@ -11,7 +11,7 @@ import { computeStateDomain } from "../../../common/entity/compute_state_domain"
import { computeStateName } from "../../../common/entity/compute_state_name";
import {
stateColorBrightness,
stateColorCss,
stateColor,
} from "../../../common/entity/state_color";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import {
@@ -203,14 +203,18 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action;
if (hvacAction in CLIMATE_HVAC_ACTION_TO_MODE) {
return stateColorCss(stateObj, CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]);
return stateColor(
this,
stateObj,
CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]
);
}
return undefined;
}
if (stateObj.attributes.rgb_color) {
return `rgb(${stateObj.attributes.rgb_color.join(",")})`;
}
const iconColor = stateColorCss(stateObj);
const iconColor = stateColor(this, stateObj);
if (iconColor) {
return iconColor;
}

View File

@@ -7,7 +7,7 @@ import { styleMap } from "lit/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import type { HumidifierEntity } from "../../../data/humidifier";
@@ -135,7 +135,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
const name = this._config!.name || computeStateName(stateObj);
const color = stateColorCss(stateObj);
const color = stateColor(this, stateObj);
const controlMaxWidth = this._resizeController.value
? `${this._resizeController.value}px`

View File

@@ -7,7 +7,7 @@ import { styleMap } from "lit/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import type { ClimateEntity } from "../../../data/climate";
@@ -127,7 +127,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
const name = this._config!.name || computeStateName(stateObj);
const color = stateColorCss(stateObj);
const color = stateColor(this, stateObj);
const controlMaxWidth = this._resizeController.value
? `${this._resizeController.value}px`

View File

@@ -11,7 +11,7 @@ import { DOMAINS_TOGGLE } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/ha-ripple";
import "../../../components/ha-state-icon";
@@ -202,7 +202,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
}
// Fallback to state color
return stateColorCss(entity);
return stateColor(this, entity);
}
);
@@ -323,7 +323,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
.stateObj=${stateObj}
.hass=${this.hass}
></ha-state-icon>
${renderTileBadge(stateObj, this.hass)}
${renderTileBadge(this, stateObj, this.hass)}
</ha-tile-icon>
<ha-tile-info
id="info"

View File

@@ -1,13 +1,17 @@
import { html, nothing } from "lit";
import { styleMap } from "lit/directives/style-map";
import { stateColorCss } from "../../../../../common/entity/state_color";
import { stateColor } from "../../../../../common/entity/state_color";
import "../../../../../components/ha-attribute-icon";
import "../../../../../components/tile/ha-tile-badge";
import type { ClimateEntity } from "../../../../../data/climate";
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../../../../data/climate";
import type { RenderBadgeFunction } from "./tile-badge";
export const renderClimateBadge: RenderBadgeFunction = (stateObj, hass) => {
export const renderClimateBadge: RenderBadgeFunction = (
element,
stateObj,
hass
) => {
const hvacAction = (stateObj as ClimateEntity).attributes.hvac_action;
if (!hvacAction || hvacAction === "off") {
@@ -17,7 +21,8 @@ export const renderClimateBadge: RenderBadgeFunction = (stateObj, hass) => {
return html`
<ha-tile-badge
style=${styleMap({
"--tile-badge-background-color": stateColorCss(
"--tile-badge-background-color": stateColor(
element,
stateObj,
CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]
),

View File

@@ -1,13 +1,17 @@
import { html, nothing } from "lit";
import { styleMap } from "lit/directives/style-map";
import { stateColorCss } from "../../../../../common/entity/state_color";
import { stateColor } from "../../../../../common/entity/state_color";
import "../../../../../components/ha-attribute-icon";
import "../../../../../components/tile/ha-tile-badge";
import type { HumidifierEntity } from "../../../../../data/humidifier";
import { HUMIDIFIER_ACTION_MODE } from "../../../../../data/humidifier";
import type { RenderBadgeFunction } from "./tile-badge";
export const renderHumidifierBadge: RenderBadgeFunction = (stateObj, hass) => {
export const renderHumidifierBadge: RenderBadgeFunction = (
element,
stateObj,
hass
) => {
const action = (stateObj as HumidifierEntity).attributes.action;
if (!action || action === "off") {
@@ -17,7 +21,8 @@ export const renderHumidifierBadge: RenderBadgeFunction = (stateObj, hass) => {
return html`
<ha-tile-badge
style=${styleMap({
"--tile-badge-background-color": stateColorCss(
"--tile-badge-background-color": stateColor(
element,
stateObj,
HUMIDIFIER_ACTION_MODE[action]
),

View File

@@ -2,7 +2,7 @@ import { mdiHome, mdiHomeExportOutline } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { html } from "lit";
import { styleMap } from "lit/directives/style-map";
import { stateColorCss } from "../../../../../common/entity/state_color";
import { stateColor } from "../../../../../common/entity/state_color";
import "../../../../../components/ha-icon";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/tile/ha-tile-badge";
@@ -20,7 +20,11 @@ function getZone(entity: HassEntity, hass: HomeAssistant) {
return zones.find((z) => state === z.attributes.friendly_name);
}
export const renderPersonBadge: RenderBadgeFunction = (stateObj, hass) => {
export const renderPersonBadge: RenderBadgeFunction = (
element,
stateObj,
hass
) => {
const zone = getZone(stateObj, hass);
const zoneIcon = zone?.attributes.icon;
@@ -29,7 +33,7 @@ export const renderPersonBadge: RenderBadgeFunction = (stateObj, hass) => {
return html`
<ha-tile-badge
style=${styleMap({
"--tile-badge-background-color": stateColorCss(stateObj),
"--tile-badge-background-color": stateColor(element, stateObj),
})}
>
<ha-icon .icon=${zoneIcon}></ha-icon>
@@ -43,7 +47,7 @@ export const renderPersonBadge: RenderBadgeFunction = (stateObj, hass) => {
return html`
<ha-tile-badge
style=${styleMap({
"--tile-badge-background-color": stateColorCss(stateObj),
"--tile-badge-background-color": stateColor(element, stateObj),
})}
>
<ha-svg-icon .path=${defaultIcon}></ha-svg-icon>

View File

@@ -13,11 +13,16 @@ import "../../../../../components/tile/ha-tile-badge";
import "../../../../../components/ha-svg-icon";
export type RenderBadgeFunction = (
element: HTMLElement,
stateObj: HassEntity,
hass: HomeAssistant
) => TemplateResult | typeof nothing;
export const renderTileBadge: RenderBadgeFunction = (stateObj, hass) => {
export const renderTileBadge: RenderBadgeFunction = (
element,
stateObj,
hass
) => {
if (stateObj.state === UNKNOWN) {
return nothing;
}
@@ -36,11 +41,11 @@ export const renderTileBadge: RenderBadgeFunction = (stateObj, hass) => {
switch (domain) {
case "person":
case "device_tracker":
return renderPersonBadge(stateObj, hass);
return renderPersonBadge(element, stateObj, hass);
case "climate":
return renderClimateBadge(stateObj, hass);
return renderClimateBadge(element, stateObj, hass);
case "humidifier":
return renderHumidifierBadge(stateObj, hass);
return renderHumidifierBadge(element, stateObj, hass);
default:
return nothing;
}

View File

@@ -9,7 +9,7 @@ import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color";
import "../../../components/ha-heading-badge";
import "../../../components/ha-state-icon";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
@@ -97,7 +97,7 @@ export class HuiEntityHeadingBadge
return rgb2hex(hsv2rgb(hsvColor));
}
// Fallback to state color
return stateColorCss(entity);
return stateColor(this, entity);
}
if (color) {

View File

@@ -230,10 +230,10 @@ class HUIRoot extends LitElement {
},
{
icon: mdiSofa,
key: "ui.panel.lovelace.menu.add_area",
key: "ui.panel.lovelace.menu.create_area",
visible: true,
action: this._addArea,
overflowAction: this._handleAddArea,
action: this._createArea,
overflowAction: this._handleCreateArea,
},
{
icon: mdiAccount,
@@ -366,6 +366,7 @@ class HUIRoot extends LitElement {
}
showListItemsDialog(this, {
title: title,
mode: this.narrow ? "bottom-sheet" : "dialog",
items: i.subItems!.map((si) => ({
iconPath: si.icon,
label: this.hass!.localize(si.key),
@@ -837,14 +838,14 @@ class HUIRoot extends LitElement {
showNewAutomationDialog(this, { mode: "automation" });
};
private _handleAddArea(ev: CustomEvent<RequestSelectedDetail>): void {
private _handleCreateArea(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._addArea();
this._createArea();
}
private _addArea = async () => {
private _createArea = async () => {
await this.hass.loadFragmentTranslation("config");
showAreaRegistryDetailDialog(this, {
createEntry: async (values) => {
@@ -854,13 +855,15 @@ class HUIRoot extends LitElement {
}
showToast(this, {
message: this.hass.localize(
"ui.panel.lovelace.menu.add_area_success"
"ui.panel.lovelace.menu.create_area_success"
),
action: {
action: () => {
navigate(`/config/areas/area/${area.area_id}`);
},
text: this.hass.localize("ui.panel.lovelace.menu.add_area_action"),
text: this.hass.localize(
"ui.panel.lovelace.menu.create_area_action"
),
},
});
},

View File

@@ -2,7 +2,7 @@ import { ResizeController } from "@lit-labs/observers/resize-controller";
import { mdiEyeOff, mdiViewGridPlus } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import { styleMap } from "lit/directives/style-map";
@@ -14,10 +14,7 @@ import "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
import type { LovelaceViewElement } from "../../../data/lovelace";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type {
LovelaceSectionConfig,
LovelaceSectionRawConfig,
} from "../../../data/lovelace/config/section";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import type { HuiBadge } from "../badges/hui-badge";
@@ -31,7 +28,6 @@ import {
getLovelaceContainerPath,
parseLovelaceCardPath,
} from "../editor/lovelace-path";
import "../sections/hui-section";
import type { HuiSection } from "../sections/hui-section";
import type { Lovelace } from "../types";
import "./hui-view-header";
@@ -50,6 +46,8 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
@property({ attribute: false }) public isStrategy = false;
@property({ attribute: false }) public sections: HuiSection[] = [];
@property({ attribute: false }) public cards: HuiCard[] = [];
@property({ attribute: false }) public badges: HuiBadge[] = [];
@@ -60,8 +58,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
@state() _dragging = false;
@query(".container") private _container?: HTMLElement;
private _columnsController = new ResizeController(this, {
callback: (entries) => {
const totalWidth = entries[0]?.contentRect.width;
@@ -92,9 +88,9 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
this._config = config;
}
private _sectionConfigKeys = new WeakMap<LovelaceSectionRawConfig, string>();
private _sectionConfigKeys = new WeakMap<HuiSection, string>();
private _getSectionKey(section: LovelaceSectionRawConfig) {
private _getSectionKey(section: HuiSection) {
if (!this._sectionConfigKeys.has(section)) {
this._sectionConfigKeys.set(section, Math.random().toString());
}
@@ -102,44 +98,52 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
}
private _computeSectionsCount() {
const sections = Array.from(
this._container?.querySelectorAll("hui-section") ?? []
);
this._sectionColumnCount =
sections
.filter((section: HuiSection) => !section.hidden)
.map((section) => section.config.column_span ?? 1)
.reduce((acc, val) => acc + val, 0) || 0;
this._sectionColumnCount = this.sections
.filter((section) => !section.hidden)
.map((section) => section.config.column_span ?? 1)
.reduce((acc, val) => acc + val, 0);
}
private _sectionVisibilityChanged = () => {
this._computeSectionsCount();
};
public updated(changedProps: PropertyValues) {
super.updated(changedProps);
connectedCallback(): void {
super.connectedCallback();
this.addEventListener(
"section-visibility-changed",
this._sectionVisibilityChanged
);
}
if (changedProps.has("_config")) {
disconnectedCallback(): void {
super.disconnectedCallback();
this.removeEventListener(
"section-visibility-changed",
this._sectionVisibilityChanged
);
}
willUpdate(changedProperties: PropertyValues<typeof this>): void {
if (changedProperties.has("sections")) {
this._computeSectionsCount();
}
}
protected render() {
if (!this.lovelace || !this._config) return nothing;
if (!this.lovelace) return nothing;
const sections = this.sections;
const totalSectionCount =
this._sectionColumnCount + (this.lovelace?.editMode ? 1 : 0);
const editMode = this.lovelace.editMode;
const maxColumnCount = this._columnsController.value ?? 1;
const sections = this._config.sections || [];
return html`
<div
class="wrapper ${classMap({
"top-margin": Boolean(this._config.top_margin),
"top-margin": Boolean(this._config?.top_margin),
})}"
>
<hui-view-header
@@ -147,7 +151,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
.badges=${this.badges}
.lovelace=${this.lovelace}
.viewIndex=${this.index}
.config=${this._config.header}
.config=${this._config?.header}
style=${styleMap({
"--max-column-count": maxColumnCount,
})}
@@ -162,7 +166,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
>
<div
class="container ${classMap({
dense: Boolean(this._config.dense_section_placement),
dense: Boolean(this._config?.dense_section_placement),
})}"
style=${styleMap({
"--total-section-count": totalSectionCount,
@@ -171,14 +175,13 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
>
${repeat(
sections,
(section: LovelaceSectionRawConfig) =>
this._getSectionKey(section),
(section: LovelaceSectionRawConfig, idx) => {
(section) => this._getSectionKey(section),
(section, idx) => {
const columnSpan = Math.min(
section.column_span || 1,
section.config.column_span || 1,
maxColumnCount
);
const rowSpan = section.row_span || 1;
const rowSpan = section.config.row_span || 1;
return html`
<div
@@ -202,15 +205,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
`
: section
}
<hui-section
.lovelace=${this.lovelace}
.hass=${this.hass}
.config=${this._config!.sections![idx]}
.viewIndex=${this.index}
.index=${idx}
.preview=${this.lovelace?.editMode || false}
@section-visibility-changed=${this._sectionVisibilityChanged}
></hui-section>
</div>
</div>
`;
@@ -249,7 +243,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
</ha-sortable>
`
: nothing}
${editMode && this._config.cards?.length
${editMode && this._config?.cards?.length
? html`
<div class="section imported-cards">
<div class="imported-card-header">

View File

@@ -12,6 +12,7 @@ import type { LovelaceViewElement } from "../../../data/lovelace";
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
import { ensureBadgeConfig } from "../../../data/lovelace/config/badge";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type {
LovelaceViewConfig,
LovelaceViewRawConfig,
@@ -38,6 +39,9 @@ import {
} from "../editor/delete-card";
import type { LovelaceCardPath } from "../editor/lovelace-path";
import { parseLovelaceCardPath } from "../editor/lovelace-path";
import { createErrorSectionConfig } from "../sections/hui-error-section";
import "../sections/hui-section";
import type { HuiSection } from "../sections/hui-section";
import { generateLovelaceViewStrategy } from "../strategies/get-strategy";
import type { Lovelace } from "../types";
import { getViewType } from "./get-view-type";
@@ -80,6 +84,8 @@ export class HUIView extends ReactiveElement {
@state() private _badges: HuiBadge[] = [];
@state() private _sections: HuiSection[] = [];
private _layoutElementType?: string;
private _layoutElement?: LovelaceViewElement;
@@ -122,6 +128,28 @@ export class HUIView extends ReactiveElement {
return element;
}
// Public to make demo happy
public createSectionElement(sectionConfig: LovelaceSectionConfig) {
const element = document.createElement("hui-section");
element.hass = this.hass;
element.lovelace = this.lovelace;
element.config = sectionConfig;
element.viewIndex = this.index;
element.preview = this.lovelace.editMode;
element.addEventListener(
"ll-rebuild",
(ev: Event) => {
// In edit mode let it go to hui-root and rebuild whole view.
if (!this.lovelace!.editMode) {
ev.stopPropagation();
this._rebuildSection(element, sectionConfig);
}
},
{ once: true }
);
return element;
}
protected createRenderRoot() {
return this;
}
@@ -233,6 +261,14 @@ export class HUIView extends ReactiveElement {
element.hass = this.hass;
});
this._sections.forEach((element) => {
try {
element.hass = this.hass;
} catch (e: any) {
this._rebuildSection(element, createErrorSectionConfig(e.message));
}
});
this._layoutElement.hass = this.hass;
}
if (changedProperties.has("narrow")) {
@@ -240,6 +276,15 @@ export class HUIView extends ReactiveElement {
}
if (changedProperties.has("lovelace")) {
this._layoutElement.lovelace = this.lovelace;
this._sections.forEach((element) => {
try {
element.hass = this.hass;
element.lovelace = this.lovelace;
element.preview = this.lovelace.editMode;
} catch (e: any) {
this._rebuildSection(element, createErrorSectionConfig(e.message));
}
});
this._cards.forEach((element) => {
element.preview = this.lovelace.editMode;
});
@@ -290,6 +335,7 @@ export class HUIView extends ReactiveElement {
this._layoutElementConfig = viewConfig;
this._createBadges(viewConfig);
this._createCards(viewConfig);
this._createSections(viewConfig);
this._layoutElement!.isStrategy = isStrategy;
this._layoutElement!.hass = this.hass;
this._layoutElement!.narrow = this.narrow;
@@ -297,6 +343,7 @@ export class HUIView extends ReactiveElement {
this._layoutElement!.index = this.index;
this._layoutElement!.cards = this._cards;
this._layoutElement!.badges = this._badges;
this._layoutElement!.sections = this._sections;
if (addLayoutElement) {
while (this.lastChild) {
@@ -427,6 +474,36 @@ export class HUIView extends ReactiveElement {
return element;
});
}
private _createSections(config: LovelaceViewConfig): void {
if (!config || !config.sections || !Array.isArray(config.sections)) {
this._sections = [];
return;
}
this._sections = config.sections.map((sectionConfig, index) => {
const element = this.createSectionElement(sectionConfig);
element.index = index;
return element;
});
}
private _rebuildSection(
sectionElToReplace: HuiSection,
config: LovelaceSectionConfig
): void {
const newSectionEl = this.createSectionElement(config);
newSectionEl.index = sectionElToReplace.index;
if (sectionElToReplace.parentElement) {
sectionElToReplace.parentElement!.replaceChild(
newSectionEl,
sectionElToReplace
);
}
this._sections = this._sections!.map((curSectionEl) =>
curSectionEl === sectionElToReplace ? newSectionEl : curSectionEl
);
}
}
declare global {

View File

@@ -3,7 +3,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { stateColorCss } from "../../common/entity/state_color";
import { stateColor } from "../../common/entity/state_color";
import { supportsFeature } from "../../common/entity/supports-feature";
import "../../components/ha-control-select";
import type { ControlSelectOption } from "../../components/ha-control-select";
@@ -71,7 +71,7 @@ export class HaStateControlAlarmControlPanelModes extends LitElement {
}
protected render() {
const color = stateColorCss(this.stateObj);
const color = stateColor(this, this.stateObj);
const modes = this._modes(this.stateObj);

View File

@@ -4,7 +4,7 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { stateActive } from "../../common/entity/state_active";
import { domainStateColorProperties } from "../../common/entity/state_color";
import { stateColor } from "../../common/entity/state_color";
import { supportsFeature } from "../../common/entity/supports-feature";
import { clamp } from "../../common/number/clamp";
import { debounce } from "../../common/util/debounce";
@@ -15,7 +15,6 @@ import "../../components/ha-svg-icon";
import type { ClimateEntity } from "../../data/climate";
import { ClimateEntityFeature } from "../../data/climate";
import { UNAVAILABLE } from "../../data/entity";
import { computeCssVariable } from "../../resources/css-variables";
import type { HomeAssistant } from "../../types";
import {
createStateControlCircularSliderController,
@@ -172,14 +171,7 @@ export class HaStateControlClimateHumidity extends LitElement {
const active = stateActive(this.stateObj);
// Use humidifier state color
const stateColor = computeCssVariable(
domainStateColorProperties(
"humidifier",
this.stateObj,
active ? "on" : "off"
)
);
const color = stateColor(this, this.stateObj);
const targetHumidity = this._targetHumidity;
const currentHumidity = this.stateObj.attributes.current_humidity;
@@ -196,7 +188,7 @@ export class HaStateControlClimateHumidity extends LitElement {
<div
class="container${containerSizeClass}"
style=${styleMap({
"--state-color": stateColor,
"--state-color": color,
})}
>
<ha-control-circular-slider

View File

@@ -6,7 +6,6 @@ import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { UNIT_F } from "../../common/const";
import { stateActive } from "../../common/entity/state_active";
import { stateColorCss } from "../../common/entity/state_color";
import { supportsFeature } from "../../common/entity/supports-feature";
import { clamp } from "../../common/number/clamp";
import { formatNumber } from "../../common/number/format_number";
@@ -28,6 +27,7 @@ import {
createStateControlCircularSliderController,
stateControlCircularSliderStyle,
} from "../state-control-circular-slider-style";
import { stateColor } from "../../common/entity/state_color";
type Target = "value" | "low" | "high";
@@ -189,8 +189,8 @@ export class HaStateControlClimateTemperature extends LitElement {
}
private _renderTemperatureButtons(target: Target, colored?: boolean) {
const lowColor = stateColorCss(this.stateObj, "heat");
const highColor = stateColorCss(this.stateObj, "cool");
const lowColor = stateColor(this, this.stateObj, "heat");
const highColor = stateColor(this, this.stateObj, "cool");
const color =
colored && stateActive(this.stateObj)
@@ -414,13 +414,14 @@ export class HaStateControlClimateTemperature extends LitElement {
const action = this.stateObj.attributes.hvac_action;
const active = stateActive(this.stateObj);
const stateColor = stateColorCss(this.stateObj);
const lowColor = stateColorCss(this.stateObj, active ? "heat" : "off");
const highColor = stateColorCss(this.stateObj, active ? "cool" : "off");
const color = stateColor(this, this.stateObj);
const lowColor = stateColor(this, this.stateObj, active ? "heat" : "off");
const highColor = stateColor(this, this.stateObj, active ? "cool" : "off");
let actionColor: string | undefined;
if (action && action !== "idle" && action !== "off" && active) {
actionColor = stateColorCss(
actionColor = stateColor(
this,
this.stateObj,
CLIMATE_HVAC_ACTION_TO_MODE[action]
);
@@ -448,7 +449,7 @@ export class HaStateControlClimateTemperature extends LitElement {
<div
class="container${containerSizeClass}"
style=${styleMap({
"--state-color": stateColor,
"--state-color": color,
"--action-color": actionColor,
})}
>

View File

@@ -3,7 +3,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display";
import { stateColorCss } from "../../common/entity/state_color";
import { stateColor } from "../../common/entity/state_color";
import "../../components/ha-control-slider";
import type { CoverEntity } from "../../data/cover";
import { UNAVAILABLE } from "../../data/entity";
@@ -37,8 +37,8 @@ export class HaStateControlCoverPosition extends LitElement {
}
protected render(): TemplateResult {
const openColor = stateColorCss(this.stateObj, "open");
const color = stateColorCss(this.stateObj);
const openColor = stateColor(this, this.stateObj, "open");
const color = stateColor(this, this.stateObj);
return html`
<ha-control-slider

View File

@@ -3,7 +3,7 @@ import { css, html, LitElement, unsafeCSS } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display";
import { stateColorCss } from "../../common/entity/state_color";
import { stateColor } from "../../common/entity/state_color";
import "../../components/ha-control-slider";
import type { CoverEntity } from "../../data/cover";
import { UNAVAILABLE } from "../../data/entity";
@@ -67,8 +67,8 @@ export class HaStateControlInfoCoverTiltPosition extends LitElement {
}
protected render(): TemplateResult {
const openColor = stateColorCss(this.stateObj, "open");
const color = stateColorCss(this.stateObj);
const openColor = stateColor(this, this.stateObj, "open");
const color = stateColor(this, this.stateObj);
return html`
<ha-control-slider

View File

@@ -4,7 +4,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { stateColorCss } from "../../common/entity/state_color";
import { stateColor } from "../../common/entity/state_color";
import "../../components/ha-control-button";
import "../../components/ha-control-switch";
import "../../components/ha-state-icon";
@@ -52,8 +52,8 @@ export class HaStateControlCoverToggle extends LitElement {
}
protected render(): TemplateResult {
const onColor = stateColorCss(this.stateObj, "open");
const offColor = stateColorCss(this.stateObj, "closed");
const onColor = stateColor(this, this.stateObj, "open");
const offColor = stateColor(this, this.stateObj, "closed");
const isOn =
this.stateObj.state === "open" ||

View File

@@ -3,7 +3,7 @@ import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display";
import { stateActive } from "../../common/entity/state_active";
import { stateColorCss } from "../../common/entity/state_color";
import { stateColor } from "../../common/entity/state_color";
import "../../components/ha-control-select";
import type { ControlSelectOption } from "../../components/ha-control-select";
import "../../components/ha-control-slider";
@@ -73,7 +73,7 @@ export class HaStateControlFanSpeed extends LitElement {
}
protected render() {
const color = stateColorCss(this.stateObj);
const color = stateColor(this, this.stateObj);
const speedCount = computeFanSpeedCount(this.stateObj);

View File

@@ -7,7 +7,7 @@ import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../common/entity/compute_domain";
import { stateActive } from "../common/entity/state_active";
import { stateColorCss } from "../common/entity/state_color";
import { stateColor } from "../common/entity/state_color";
import "../components/ha-control-button";
import "../components/ha-control-switch";
import { UNAVAILABLE, UNKNOWN } from "../data/entity";
@@ -65,8 +65,8 @@ export class HaStateControlToggle extends LitElement {
}
protected render(): TemplateResult {
const onColor = stateColorCss(this.stateObj, "on");
const offColor = stateColorCss(this.stateObj, "off");
const onColor = stateColor(this, this.stateObj, "on");
const offColor = stateColor(this, this.stateObj, "off");
const isOn = this.stateObj.state === "on";
const isOff = this.stateObj.state === "off";

View File

@@ -4,7 +4,7 @@ import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { stateActive } from "../../common/entity/state_active";
import { stateColorCss } from "../../common/entity/state_color";
import { stateColor } from "../../common/entity/state_color";
import { clamp } from "../../common/number/clamp";
import { debounce } from "../../common/util/debounce";
import "../../components/ha-big-number";
@@ -248,14 +248,15 @@ export class HaStateControlHumidifierHumidity extends LitElement {
}
protected render() {
const stateColor = stateColorCss(this.stateObj);
const color = stateColor(this, this.stateObj);
const active = stateActive(this.stateObj);
const action = this.stateObj.attributes.action;
let actionColor: string | undefined;
if (action && action !== "idle" && action !== "off" && active) {
actionColor = stateColorCss(
actionColor = stateColor(
this,
this.stateObj,
HUMIDIFIER_ACTION_MODE[action]
);
@@ -277,7 +278,7 @@ export class HaStateControlHumidifierHumidity extends LitElement {
<div
class="container${containerSizeClass}"
style=${styleMap({
"--state-color": stateColor,
"--state-color": color,
"--action-color": actionColor,
})}
>
@@ -303,7 +304,7 @@ export class HaStateControlHumidifierHumidity extends LitElement {
<div
class="container${containerSizeClass}"
style=${styleMap({
"--state-color": stateColor,
"--state-color": color,
"--action-color": actionColor,
})}
>

View File

@@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../common/color/convert-color";
import { stateActive } from "../../common/entity/state_active";
import { stateColorCss } from "../../common/entity/state_color";
import { stateColor } from "../../common/entity/state_color";
import "../../components/ha-control-slider";
import { UNAVAILABLE } from "../../data/entity";
import type { LightEntity } from "../../data/light";
@@ -41,7 +41,7 @@ export class HaStateControlLightBrightness extends LitElement {
}
protected render(): TemplateResult {
let color = stateColorCss(this.stateObj);
let color = stateColor(this, this.stateObj);
if (this.stateObj.attributes.rgb_color) {
const hsvColor = rgb2hsv(this.stateObj.attributes.rgb_color);

View File

@@ -3,7 +3,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { stateColorCss } from "../../common/entity/state_color";
import { stateColor } from "../../common/entity/state_color";
import "../../components/ha-control-button";
import "../../components/ha-control-switch";
import "../../components/ha-state-icon";
@@ -82,7 +82,7 @@ export class HaStateControlLockToggle extends LitElement {
const locking = this.stateObj.state === "locking";
const unlocking = this.stateObj.state === "unlocking";
const color = stateColorCss(this.stateObj);
const color = stateColor(this, this.stateObj);
if (this.stateObj.state === UNKNOWN) {
return html`

View File

@@ -3,7 +3,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display";
import { stateColorCss } from "../../common/entity/state_color";
import { stateColor } from "../../common/entity/state_color";
import "../../components/ha-control-slider";
import type { CoverEntity } from "../../data/cover";
import { UNAVAILABLE } from "../../data/entity";
@@ -37,7 +37,7 @@ export class HaStateControlValvePosition extends LitElement {
}
protected render(): TemplateResult {
const color = stateColorCss(this.stateObj);
const color = stateColor(this, this.stateObj);
return html`
<ha-control-slider

View File

@@ -4,7 +4,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { stateColorCss } from "../../common/entity/state_color";
import { stateColor } from "../../common/entity/state_color";
import "../../components/ha-control-button";
import "../../components/ha-control-switch";
import "../../components/ha-state-icon";
@@ -52,8 +52,8 @@ export class HaStateControlValveToggle extends LitElement {
}
protected render(): TemplateResult {
const onColor = stateColorCss(this.stateObj, "open");
const offColor = stateColorCss(this.stateObj, "closed");
const onColor = stateColor(this, this.stateObj, "open");
const offColor = stateColor(this, this.stateObj, "closed");
const isOn =
this.stateObj.state === "open" ||

View File

@@ -5,7 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { UNIT_F } from "../../common/const";
import { stateActive } from "../../common/entity/state_active";
import { stateColorCss } from "../../common/entity/state_color";
import { stateColor } from "../../common/entity/state_color";
import { supportsFeature } from "../../common/entity/supports-feature";
import { clamp } from "../../common/number/clamp";
import { debounce } from "../../common/util/debounce";
@@ -179,7 +179,7 @@ export class HaStateControlWaterHeaterTemperature extends LitElement {
WaterHeaterEntityFeature.TARGET_TEMPERATURE
);
const stateColor = stateColorCss(this.stateObj);
const color = stateColor(this, this.stateObj);
const active = stateActive(this.stateObj);
const containerSizeClass = this._sizeController.value
@@ -195,7 +195,7 @@ export class HaStateControlWaterHeaterTemperature extends LitElement {
<div
class="container${containerSizeClass}"
style=${styleMap({
"--state-color": stateColor,
"--state-color": color,
})}
>
<ha-control-circular-slider
@@ -226,7 +226,7 @@ export class HaStateControlWaterHeaterTemperature extends LitElement {
<div
class="container${containerSizeClass}"
style=${styleMap({
"--state-color": stateColor,
"--state-color": color,
})}
>
<ha-control-circular-slider

View File

@@ -7059,9 +7059,9 @@
"add": "Add to Home Assistant",
"add_device": "Add device",
"create_automation": "Create automation",
"add_area": "Add area",
"add_area_success": "Area added",
"add_area_action": "View area",
"create_area": "Create area",
"create_area_success": "Area created",
"create_area_action": "View area",
"add_person_success": "Person added",
"add_person_action": "View persons",
"add_person": "Add person"

View File

@@ -1,31 +1,31 @@
import { describe, it, expect } from "vitest";
import { batteryStateColorProperty } from "../../../../src/common/entity/color/battery_color";
import { batteryStateColor } from "../../../../src/common/entity/color/battery_color";
describe("battery_color", () => {
it("should return green for high battery level", () => {
let color = batteryStateColorProperty("70");
let color = batteryStateColor("70");
expect(color).toBe("--state-sensor-battery-high-color");
color = batteryStateColorProperty("200");
color = batteryStateColor("200");
expect(color).toBe("--state-sensor-battery-high-color");
});
it("should return yellow for medium battery level", () => {
let color = batteryStateColorProperty("69.99");
let color = batteryStateColor("69.99");
expect(color).toBe("--state-sensor-battery-medium-color");
color = batteryStateColorProperty("30");
color = batteryStateColor("30");
expect(color).toBe("--state-sensor-battery-medium-color");
});
it("should return red for low battery level", () => {
let color = batteryStateColorProperty("29.999");
let color = batteryStateColor("29.999");
expect(color).toBe("--state-sensor-battery-low-color");
color = batteryStateColorProperty("-20");
color = batteryStateColor("-20");
expect(color).toBe("--state-sensor-battery-low-color");
});
// add nan test
it("should return undefined for non-numeric state", () => {
const color = batteryStateColorProperty("not a number");
const color = batteryStateColor("not a number");
expect(color).toBe(undefined);
});
});

View File

@@ -4752,16 +4752,6 @@ __metadata:
languageName: node
linkType: hard
"@types/node-fetch@npm:^2.6.12":
version: 2.6.13
resolution: "@types/node-fetch@npm:2.6.13"
dependencies:
"@types/node": "npm:*"
form-data: "npm:^4.0.4"
checksum: 10/944d52214791ebba482ca1393a4f0d62b0dbac5f7343ff42c128b75d5356d8bcefd4df77771b55c1acd19d118e16e9bd5d2792819c51bc13402d1c87c0975435
languageName: node
linkType: hard
"@types/node-forge@npm:^1.3.0":
version: 1.3.14
resolution: "@types/node-forge@npm:1.3.14"
@@ -9467,7 +9457,7 @@ __metadata:
ts-lit-plugin: "npm:2.0.2"
typescript: "npm:5.9.2"
typescript-eslint: "npm:8.43.0"
ua-parser-js: "npm:2.0.4"
ua-parser-js: "npm:2.0.5"
vite-tsconfig-paths: "npm:5.1.4"
vitest: "npm:3.2.4"
vue: "npm:2.7.16"
@@ -11510,7 +11500,7 @@ __metadata:
languageName: node
linkType: hard
"node-fetch@npm:^2.6.1, node-fetch@npm:^2.7.0":
"node-fetch@npm:^2.6.1":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
@@ -14560,18 +14550,17 @@ __metadata:
languageName: node
linkType: hard
"ua-parser-js@npm:2.0.4":
version: 2.0.4
resolution: "ua-parser-js@npm:2.0.4"
"ua-parser-js@npm:2.0.5":
version: 2.0.5
resolution: "ua-parser-js@npm:2.0.5"
dependencies:
"@types/node-fetch": "npm:^2.6.12"
detect-europe-js: "npm:^0.1.2"
is-standalone-pwa: "npm:^0.1.1"
node-fetch: "npm:^2.7.0"
ua-is-frozen: "npm:^0.1.2"
undici: "npm:^7.12.0"
bin:
ua-parser-js: script/cli.js
checksum: 10/eb3a57cd4aea6c42d2d766761ccf38cdc4576075646dec611efc336f0d1e640896ec4ca084142a1fedbf25c589e093e2cad50c49a22d089e234029ecb9b8d2e4
checksum: 10/e946cb1c85bfcd0f2d30c7d5e1b605e340bb458432e7e87fc4aa1b2f90117e4220521d4e0bc7dd8c2a5cadd0935dedb5ac434b70efdc0007221288c1d98b3cd5
languageName: node
linkType: hard
@@ -14634,6 +14623,13 @@ __metadata:
languageName: node
linkType: hard
"undici@npm:^7.12.0":
version: 7.16.0
resolution: "undici@npm:7.16.0"
checksum: 10/2bb71672b23d3dc0f56f1b7fb6c936e4487a350db46eaafc03f2f9107f99cdf8e51ecdd32e589e2381ef47a64b6369cfb31f328b2c3ea663023aa47bc5258b9e
languageName: node
linkType: hard
"unicode-canonical-property-names-ecmascript@npm:^2.0.0":
version: 2.0.1
resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.1"