mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-17 17:09:43 +00:00
Compare commits
96 Commits
update-typ
...
fix-media-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bf7fe5caf8 | ||
![]() |
b0d4c699db | ||
![]() |
c07bf68161 | ||
![]() |
d1a0eaece5 | ||
![]() |
f608783551 | ||
![]() |
dddba58d38 | ||
![]() |
ebc16d6520 | ||
![]() |
4ed8ecad01 | ||
![]() |
c26fb1713d | ||
![]() |
2b7b17625e | ||
![]() |
cd3e4f55e2 | ||
![]() |
1c12aea8f6 | ||
![]() |
3722f971ca | ||
![]() |
409f665641 | ||
![]() |
5b3b17ef6d | ||
![]() |
05b49e8c80 | ||
![]() |
2dbdbb4b64 | ||
![]() |
a825b632bf | ||
![]() |
f8d706277d | ||
![]() |
221bc732fb | ||
![]() |
055c18463c | ||
![]() |
ddd51ff097 | ||
![]() |
55c75096d0 | ||
![]() |
fd3502f3bc | ||
![]() |
7d6bec01ae | ||
![]() |
0aeb8fa75c | ||
![]() |
456a44fdfd | ||
![]() |
53a0b311de | ||
![]() |
ded5ade0f2 | ||
![]() |
5d2d6dcd6c | ||
![]() |
92353ebed5 | ||
![]() |
bc582db7fc | ||
![]() |
ab415188ba | ||
![]() |
29c11978b3 | ||
![]() |
574f9e8936 | ||
![]() |
fddc00bfab | ||
![]() |
ff5cbb0613 | ||
![]() |
498d933c06 | ||
![]() |
f9fbb254bf | ||
![]() |
536602580d | ||
![]() |
c111bf1062 | ||
![]() |
0242fbc6f8 | ||
![]() |
f65a0ef4f7 | ||
![]() |
6a333a4774 | ||
![]() |
7742ccf631 | ||
![]() |
20f2a8d53e | ||
![]() |
ec9fbe7d77 | ||
![]() |
6fa226d30a | ||
![]() |
b76a723fd9 | ||
![]() |
5237cc72b7 | ||
![]() |
63d2718f67 | ||
![]() |
1e26f155a7 | ||
![]() |
27e13017c3 | ||
![]() |
88f1dc9c16 | ||
![]() |
825e707a80 | ||
![]() |
0bcaa104e7 | ||
![]() |
6b3f807129 | ||
![]() |
c464d344db | ||
![]() |
69f0a4a728 | ||
![]() |
2ba8f9f99d | ||
![]() |
7e06bbc467 | ||
![]() |
6017d82c21 | ||
![]() |
40c200a172 | ||
![]() |
a2f70f682f | ||
![]() |
c42a899b52 | ||
![]() |
706f43e99e | ||
![]() |
f5496c21e8 | ||
![]() |
34dce5b279 | ||
![]() |
a4f07423ec | ||
![]() |
9e32c24f3c | ||
![]() |
b281d095cd | ||
![]() |
fe7e8e17ae | ||
![]() |
2161357226 | ||
![]() |
e8e65a4293 | ||
![]() |
724adab2d6 | ||
![]() |
345ad6c9c5 | ||
![]() |
a88d066d7e | ||
![]() |
a8e5c8482b | ||
![]() |
d5ff8ab1e1 | ||
![]() |
e765cc10fb | ||
![]() |
916dec101f | ||
![]() |
909fc119b7 | ||
![]() |
8751dc46f4 | ||
![]() |
118c25d25f | ||
![]() |
ae5427a75e | ||
![]() |
3b6e267fb5 | ||
![]() |
1770a51303 | ||
![]() |
534df3d378 | ||
![]() |
23229b3e3b | ||
![]() |
94ee99160b | ||
![]() |
b009d71e8f | ||
![]() |
2ab8209622 | ||
![]() |
ed2940edc3 | ||
![]() |
e2b9a06242 | ||
![]() |
a7acee0438 | ||
![]() |
1208af510c |
@@ -302,7 +302,7 @@ export class HcConnect extends LitElement {
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error a {
|
||||
|
@@ -86,9 +86,9 @@ class HcLayout extends LitElement {
|
||||
.card-header {
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
|
||||
font-size: var(--ha-card-header-font-size, 24px);
|
||||
letter-spacing: -0.012em;
|
||||
line-height: var(--ha-line-height-normal);
|
||||
line-height: 32px;
|
||||
padding: 24px 16px 16px;
|
||||
display: block;
|
||||
margin: 0;
|
||||
@@ -98,7 +98,7 @@ class HcLayout extends LitElement {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-size: 14px;
|
||||
color: var(--secondary-text-color);
|
||||
line-height: initial;
|
||||
}
|
||||
@@ -113,7 +113,7 @@ class HcLayout extends LitElement {
|
||||
}
|
||||
|
||||
:host ::slotted(.section-header) {
|
||||
font-weight: var(--ha-font-weight-semibold);
|
||||
font-weight: 500;
|
||||
padding: 4px 16px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -135,7 +135,7 @@ class HcLayout extends LitElement {
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-size: 12px;
|
||||
padding: 8px 0 24px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@ class HcLaunchScreen extends LitElement {
|
||||
display: block;
|
||||
height: 100vh;
|
||||
background-color: #f2f4f9;
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
font-size: 24px;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
|
@@ -50,7 +50,7 @@
|
||||
font-family: Roboto, Noto, sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-weight: 400;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
@@ -37,13 +37,13 @@ class PageDescription extends HaMarkdown {
|
||||
border-bottom: 1px solid var(--secondary-background-color);
|
||||
}
|
||||
.title {
|
||||
font-size: var(--ha-font-size-5xl);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
font-size: 42px;
|
||||
line-height: 56px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: var(--ha-font-size-l);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.root {
|
||||
max-width: 800px;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import "./ha-gallery";
|
||||
|
||||
import("../../src/resources/ha-style");
|
||||
import("../../src/resources/append-ha-style");
|
||||
|
||||
document.body.appendChild(document.createElement("ha-gallery"));
|
||||
|
@@ -34,7 +34,7 @@ class HaDemoOptions extends LitElement {
|
||||
height: 64px;
|
||||
padding: 0 16px;
|
||||
pointer-events: none;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-size: 20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -250,14 +250,14 @@ class HaGallery extends LitElement {
|
||||
}
|
||||
|
||||
.page-footer .header {
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-semibold);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-footer .secondary {
|
||||
line-height: var(--ha-line-height-normal);
|
||||
line-height: 23px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
65
gallery/src/pages/components/ha-badge.markdown
Normal file
65
gallery/src/pages/components/ha-badge.markdown
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: Badge
|
||||
subtitle: Lovelace dashboard badge
|
||||
---
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
# Badge `<ha-badge>`
|
||||
|
||||
The badge component is a small component that displays a number or status information. It is used in the lovelace dashboard on the top.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Example Usage
|
||||
|
||||
<div class="wrapper">
|
||||
<ha-badge>
|
||||
simple badge
|
||||
</ha-badge>
|
||||
|
||||
<ha-badge label="Info">
|
||||
With a label
|
||||
</ha-badge>
|
||||
|
||||
<ha-badge type="button">
|
||||
Type button
|
||||
</ha-badge>
|
||||
</div>
|
||||
|
||||
```html
|
||||
<ha-badge> simple badge </ha-badge>
|
||||
|
||||
<ha-badge label="Info"> With a label </ha-badge>
|
||||
|
||||
<ha-badge type="button"> Type button </ha-badge>
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
**Slots**
|
||||
|
||||
- default slot is the content of the badge
|
||||
- no default
|
||||
- `icon` set the icon of the badge
|
||||
- no default
|
||||
|
||||
**Properties/Attributes**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| -------- | ----------------------- | ----------- | ------------------------------------------------------------ |
|
||||
| type | `"badge"` or `"button"` | `"badge"` | If it's button it shows a ripple effect |
|
||||
| label | string | `undefined` | Text label for the badge, only visible if `iconOnly = false` |
|
||||
| iconOnly | boolean | `false` | Only show label |
|
||||
|
||||
**CSS Custom Properties**
|
||||
|
||||
- `--ha-badge-size` (default `36px`)
|
||||
- `--ha-badge-border-radius` (default `calc(var(--ha-badge-size, 36px) / 2)`)
|
||||
- `--ha-badge-font-size` (default `var(--ha-font-size-s)`)
|
||||
- `--ha-badge-icon-size` (default `18px`)
|
129
gallery/src/pages/components/ha-badge.ts
Normal file
129
gallery/src/pages/components/ha-badge.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { mdiButtonCursor, mdiHome } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-badge";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
|
||||
|
||||
const badges: {
|
||||
type?: "badge" | "button";
|
||||
label?: string;
|
||||
iconOnly?: boolean;
|
||||
slot?: TemplateResult;
|
||||
iconSlot?: TemplateResult;
|
||||
}[] = [
|
||||
{
|
||||
slot: html`<span>Badge</span>`,
|
||||
},
|
||||
{
|
||||
type: "badge",
|
||||
label: "Badge",
|
||||
iconSlot: html`<ha-svg-icon slot="icon" .path=${mdiHome}></ha-svg-icon>`,
|
||||
slot: html`<span>Badge</span>`,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
label: "Button",
|
||||
iconSlot: html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiButtonCursor}
|
||||
></ha-svg-icon>`,
|
||||
slot: html`<span>Button</span>`,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
label: "Label only",
|
||||
iconSlot: html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiButtonCursor}
|
||||
></ha-svg-icon>`,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
label: "Label",
|
||||
slot: html`<span>Button no label</span>`,
|
||||
},
|
||||
{
|
||||
label: "Icon only",
|
||||
iconOnly: true,
|
||||
iconSlot: html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiHomeAssistant}
|
||||
></ha-svg-icon>`,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-badge")
|
||||
export class DemoHaBadge extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-badge ${mode} demo">
|
||||
<div class="card-content">
|
||||
${badges.map(
|
||||
(badge) => html`
|
||||
<ha-badge
|
||||
.type=${badge.type || undefined}
|
||||
.label=${badge.label}
|
||||
.iconOnly=${badge.iconOnly || false}
|
||||
>
|
||||
${badge.iconSlot} ${badge.slot}
|
||||
</ha-badge>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-badge": DemoHaBadge;
|
||||
}
|
||||
}
|
@@ -150,7 +150,7 @@ export class DemoHaBarButton extends LitElement {
|
||||
margin: 0;
|
||||
}
|
||||
label {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: 600;
|
||||
}
|
||||
.custom {
|
||||
--control-button-icon-color: var(--primary-color);
|
||||
|
@@ -86,7 +86,7 @@ export class DemoHarControlNumberButtons extends LitElement {
|
||||
margin: 0;
|
||||
}
|
||||
label {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: 600;
|
||||
}
|
||||
.custom {
|
||||
color: #2196f3;
|
||||
|
@@ -125,7 +125,7 @@ export class DemoHaControlSelectMenu extends LitElement {
|
||||
margin: 0;
|
||||
}
|
||||
label {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: 600;
|
||||
}
|
||||
.custom {
|
||||
--control-button-icon-color: var(--primary-color);
|
||||
|
@@ -181,7 +181,7 @@ export class DemoHaControlSelect extends LitElement {
|
||||
margin: 0;
|
||||
}
|
||||
label {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: 600;
|
||||
}
|
||||
.custom {
|
||||
--mdc-icon-size: 24px;
|
||||
|
@@ -144,7 +144,7 @@ export class DemoHaBarSlider extends LitElement {
|
||||
margin: 0;
|
||||
}
|
||||
label {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: 600;
|
||||
}
|
||||
.custom {
|
||||
--control-slider-color: #ffcf4c;
|
||||
|
@@ -112,7 +112,7 @@ export class DemoHaControlSwitch extends LitElement {
|
||||
margin: 0;
|
||||
}
|
||||
label {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: 600;
|
||||
}
|
||||
.custom {
|
||||
--control-switch-on-color: var(--green-color);
|
||||
|
@@ -105,8 +105,8 @@ export class DemoHaHsColorPicker extends LitElement {
|
||||
width: 400px;
|
||||
}
|
||||
.value {
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
`;
|
||||
|
@@ -123,7 +123,7 @@ export class DemoHaSelectBox extends LitElement {
|
||||
margin: 0;
|
||||
}
|
||||
label {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeNumeric extends LitElement {
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeSeconds extends LitElement {
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeShortYear extends LitElement {
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeShort extends LitElement {
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTime extends LitElement {
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
|
@@ -92,7 +92,7 @@ export class DemoDateTimeDate extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
.header {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeTimeSeconds extends LitElement {
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeTimeWeekday extends LitElement {
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeTime extends LitElement {
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
|
@@ -428,13 +428,13 @@ class HassioAddonConfig extends LitElement {
|
||||
.header h2 {
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
|
||||
font-size: var(--ha-card-header-font-size, 24px);
|
||||
letter-spacing: -0.012em;
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
line-height: 48px;
|
||||
padding: 12px 16px 16px;
|
||||
display: block;
|
||||
margin-block: 0px;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-weight: normal;
|
||||
}
|
||||
.card-actions.right {
|
||||
justify-content: flex-end;
|
||||
|
@@ -1280,12 +1280,12 @@ class HassioAddonInfo extends LitElement {
|
||||
padding-left: 8px;
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-end: initial;
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
font-size: 24px;
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
}
|
||||
.addon-version {
|
||||
float: var(--float-end);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-size: 15px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.errors {
|
||||
|
@@ -391,7 +391,7 @@ export class HassioBackups extends LitElement {
|
||||
top: -4px;
|
||||
}
|
||||
.selected-txt {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: bold;
|
||||
padding-left: 16px;
|
||||
padding-inline-start: 16px;
|
||||
padding-inline-end: initial;
|
||||
@@ -401,7 +401,7 @@ export class HassioBackups extends LitElement {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.header-toolbar .selected-txt {
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-size: 16px;
|
||||
}
|
||||
.header-toolbar .header-btns {
|
||||
margin-right: -12px;
|
||||
|
@@ -101,7 +101,7 @@ class HassioCardContent extends LitElement {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 2.4em;
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
line-height: 1.2em;
|
||||
}
|
||||
.icon_image img {
|
||||
max-height: 40px;
|
||||
|
@@ -131,7 +131,7 @@ export class HassioUpdate extends LitElement {
|
||||
}
|
||||
.update-heading {
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-semibold);
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5em;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
@@ -169,7 +169,7 @@ class HassioHardwareDialog extends LitElement {
|
||||
pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
line-height: var(--ha-line-height-normal);
|
||||
line-height: 1.45;
|
||||
font-family: var(--ha-font-family-code);
|
||||
}
|
||||
code {
|
||||
|
@@ -8,7 +8,7 @@ body {
|
||||
font-family: Roboto, sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
|
@@ -340,12 +340,12 @@ class HassioIngressView extends LitElement {
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-size: 16px;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
pointer-events: none;
|
||||
background-color: var(--app-header-background-color);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-weight: 400;
|
||||
color: var(--app-header-text-color, white);
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
box-sizing: border-box;
|
||||
@@ -354,7 +354,7 @@ class HassioIngressView extends LitElement {
|
||||
|
||||
.main-title {
|
||||
margin: var(--margin-title);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
line-height: 20px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
@@ -1,3 +1,3 @@
|
||||
import "./ha-landing-page";
|
||||
|
||||
import("../../src/resources/ha-style");
|
||||
import("../../src/resources/append-ha-style");
|
||||
|
12
package.json
12
package.json
@@ -89,8 +89,8 @@
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.8.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vaadin/combo-box": "24.7.4",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.4",
|
||||
"@vaadin/combo-box": "24.7.5",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.5",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
@@ -160,8 +160,8 @@
|
||||
"@octokit/plugin-retry": "7.2.1",
|
||||
"@octokit/rest": "21.1.1",
|
||||
"@rsdoctor/rspack-plugin": "1.0.2",
|
||||
"@rspack/cli": "1.3.7",
|
||||
"@rspack/core": "1.3.7",
|
||||
"@rspack/cli": "1.3.8",
|
||||
"@rspack/core": "1.3.8",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.21",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -193,7 +193,7 @@
|
||||
"eslint-plugin-lit": "2.1.1",
|
||||
"eslint-plugin-lit-a11y": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-wc": "3.0.0",
|
||||
"eslint-plugin-wc": "3.0.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.0",
|
||||
"glob": "11.0.2",
|
||||
@@ -219,7 +219,7 @@
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.31.0",
|
||||
"typescript-eslint": "8.31.1",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.1.2",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20250326.0"
|
||||
version = "20250430.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
@@ -101,7 +101,7 @@ export class HaAuthFlow extends LitElement {
|
||||
a.forgot-password {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.space-between {
|
||||
display: flex;
|
||||
|
@@ -93,8 +93,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
}
|
||||
p {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.card-content {
|
||||
background: var(
|
||||
@@ -151,8 +151,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
h1 {
|
||||
font-size: var(--ha-font-size-3xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
@@ -57,9 +57,9 @@ export class HaPickAuthProvider extends LitElement {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
h3:before {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
|
30
src/common/dom/can-override-input.ts
Normal file
30
src/common/dom/can-override-input.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export const canOverrideAlphanumericInput = (composedPath: EventTarget[]) => {
|
||||
if (composedPath.some((el) => "tagName" in el && el.tagName === "HA-MENU")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const el = composedPath[0] as Element;
|
||||
|
||||
if (el.tagName === "TEXTAREA") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (el.parentElement?.tagName === "HA-SELECT") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (el.tagName !== "INPUT") {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch ((el as HTMLInputElement).type) {
|
||||
case "button":
|
||||
case "checkbox":
|
||||
case "hidden":
|
||||
case "radio":
|
||||
case "range":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
@@ -6,34 +6,15 @@ interface AreaContext {
|
||||
area: AreaRegistryEntry | null;
|
||||
floor: FloorRegistryEntry | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the context of a specific area, including its associated area registry entry
|
||||
* and floor registry entry, if available.
|
||||
*
|
||||
* @param areaId - The unique identifier of the area to retrieve context for.
|
||||
* @param hass - The Home Assistant instance containing area and floor registry data.
|
||||
* @returns An object containing the area registry entry and the associated floor registry entry,
|
||||
* or `null` values if the area or floor is not found.
|
||||
*/
|
||||
export const getAreaContext = (
|
||||
areaId: string,
|
||||
area: AreaRegistryEntry,
|
||||
hass: HomeAssistant
|
||||
): AreaContext => {
|
||||
const area = (hass.areas[areaId] as AreaRegistryEntry | undefined) || null;
|
||||
|
||||
if (!area) {
|
||||
return {
|
||||
area: null,
|
||||
floor: null,
|
||||
};
|
||||
}
|
||||
|
||||
const floorId = area?.floor_id;
|
||||
const floor = floorId ? hass.floors[floorId] : null;
|
||||
const floorId = area.floor_id;
|
||||
const floor = floorId ? hass.floors[floorId] : undefined;
|
||||
|
||||
return {
|
||||
area: area,
|
||||
floor: floor,
|
||||
floor: floor || null,
|
||||
};
|
||||
};
|
||||
|
26
src/common/entity/context/get_device_context.ts
Normal file
26
src/common/entity/context/get_device_context.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
interface DeviceContext {
|
||||
device: DeviceRegistryEntry;
|
||||
area: AreaRegistryEntry | null;
|
||||
floor: FloorRegistryEntry | null;
|
||||
}
|
||||
|
||||
export const getDeviceContext = (
|
||||
device: DeviceRegistryEntry,
|
||||
hass: HomeAssistant
|
||||
): DeviceContext => {
|
||||
const areaId = device.area_id;
|
||||
const area = areaId ? hass.areas[areaId] : undefined;
|
||||
const floorId = area?.floor_id;
|
||||
const floor = floorId ? hass.floors[floorId] : undefined;
|
||||
|
||||
return {
|
||||
device: device,
|
||||
area: area || null,
|
||||
floor: floor || null,
|
||||
};
|
||||
};
|
@@ -1,6 +1,11 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import type { EntityRegistryDisplayEntry } from "../../../data/entity_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
ExtEntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@@ -11,27 +16,15 @@ interface EntityContext {
|
||||
floor: FloorRegistryEntry | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the context of an entity, including its associated device, area, and floor.
|
||||
*
|
||||
* @param entityId - The unique identifier of the entity to retrieve the context for.
|
||||
* @param hass - The Home Assistant object containing the registry data for entities, devices, areas, and floors.
|
||||
* @returns An object containing the entity, its associated device, area, and floor, or `null` for each if not found.
|
||||
*
|
||||
* The returned `EntityContext` object includes:
|
||||
* - `entity`: The entity registry entry, or `null` if the entity is not found.
|
||||
* - `device`: The device registry entry associated with the entity, or `null` if not found.
|
||||
* - `area`: The area registry entry associated with the entity or device, or `null` if not found.
|
||||
* - `floor`: The floor registry entry associated with the area, or `null` if not found.
|
||||
*/
|
||||
export const getEntityContext = (
|
||||
entityId: string,
|
||||
stateObj: HassEntity,
|
||||
hass: HomeAssistant
|
||||
): EntityContext => {
|
||||
const entity =
|
||||
(hass.entities[entityId] as EntityRegistryDisplayEntry | undefined) || null;
|
||||
const entry = hass.entities[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
|
||||
if (!entity) {
|
||||
if (!entry) {
|
||||
return {
|
||||
entity: null,
|
||||
device: null,
|
||||
@@ -39,18 +32,28 @@ export const getEntityContext = (
|
||||
floor: null,
|
||||
};
|
||||
}
|
||||
return getEntityEntryContext(entry, hass);
|
||||
};
|
||||
|
||||
const deviceId = entity?.device_id;
|
||||
const device = deviceId ? hass.devices[deviceId] : null;
|
||||
const areaId = entity?.area_id || device?.area_id;
|
||||
const area = areaId ? hass.areas[areaId] : null;
|
||||
export const getEntityEntryContext = (
|
||||
entry:
|
||||
| EntityRegistryDisplayEntry
|
||||
| EntityRegistryEntry
|
||||
| ExtEntityRegistryEntry,
|
||||
hass: HomeAssistant
|
||||
): EntityContext => {
|
||||
const entity = hass.entities[entry.entity_id];
|
||||
const deviceId = entry?.device_id;
|
||||
const device = deviceId ? hass.devices[deviceId] : undefined;
|
||||
const areaId = entry?.area_id || device?.area_id;
|
||||
const area = areaId ? hass.areas[areaId] : undefined;
|
||||
const floorId = area?.floor_id;
|
||||
const floor = floorId ? hass.floors[floorId] : null;
|
||||
const floor = floorId ? hass.floors[floorId] : undefined;
|
||||
|
||||
return {
|
||||
entity: entity,
|
||||
device: device,
|
||||
area: area,
|
||||
floor: floor,
|
||||
device: device || null,
|
||||
area: area || null,
|
||||
floor: floor || null,
|
||||
};
|
||||
};
|
||||
|
@@ -60,7 +60,7 @@ export const generateEntityFilter = (
|
||||
}
|
||||
}
|
||||
|
||||
const { area, floor, device, entity } = getEntityContext(entityId, hass);
|
||||
const { area, floor, device, entity } = getEntityContext(stateObj, hass);
|
||||
|
||||
if (entity && entity.hidden) {
|
||||
return false;
|
||||
|
@@ -1,18 +0,0 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
interface AreaContext {
|
||||
floor: FloorRegistryEntry | null;
|
||||
}
|
||||
export const getAreaContext = (
|
||||
area: AreaRegistryEntry,
|
||||
hass: HomeAssistant
|
||||
): AreaContext => {
|
||||
const floorId = area.floor_id;
|
||||
const floor = floorId ? hass.floors[floorId] : null;
|
||||
|
||||
return {
|
||||
floor: floor,
|
||||
};
|
||||
};
|
@@ -1,24 +0,0 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
interface DeviceContext {
|
||||
area: AreaRegistryEntry | null;
|
||||
floor: FloorRegistryEntry | null;
|
||||
}
|
||||
|
||||
export const getDeviceContext = (
|
||||
device: DeviceRegistryEntry,
|
||||
hass: HomeAssistant
|
||||
): DeviceContext => {
|
||||
const areaId = device.area_id;
|
||||
const area = areaId ? hass.areas[areaId] : null;
|
||||
const floorId = area?.floor_id;
|
||||
const floor = floorId ? hass.floors[floorId] : null;
|
||||
|
||||
return {
|
||||
area: area,
|
||||
floor: floor,
|
||||
};
|
||||
};
|
@@ -1,55 +0,0 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
ExtEntityRegistryEntry,
|
||||
} from "../../data/entity_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
interface EntityContext {
|
||||
device: DeviceRegistryEntry | null;
|
||||
area: AreaRegistryEntry | null;
|
||||
floor: FloorRegistryEntry | null;
|
||||
}
|
||||
|
||||
export const getEntityContext = (
|
||||
stateObj: HassEntity,
|
||||
hass: HomeAssistant
|
||||
): EntityContext => {
|
||||
const entry = hass.entities[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
|
||||
if (!entry) {
|
||||
return {
|
||||
device: null,
|
||||
area: null,
|
||||
floor: null,
|
||||
};
|
||||
}
|
||||
return getEntityEntryContext(entry, hass);
|
||||
};
|
||||
|
||||
export const getEntityEntryContext = (
|
||||
entry:
|
||||
| EntityRegistryDisplayEntry
|
||||
| EntityRegistryEntry
|
||||
| ExtEntityRegistryEntry,
|
||||
hass: HomeAssistant
|
||||
): EntityContext => {
|
||||
const deviceId = entry?.device_id;
|
||||
const device = deviceId ? hass.devices[deviceId] : null;
|
||||
const areaId = entry?.area_id || device?.area_id;
|
||||
const area = areaId ? hass.areas[areaId] : null;
|
||||
const floorId = area?.floor_id;
|
||||
const floor = floorId ? hass.floors[floorId] : null;
|
||||
|
||||
return {
|
||||
device: device,
|
||||
area: area,
|
||||
floor: floor,
|
||||
};
|
||||
};
|
@@ -96,7 +96,7 @@ const customGenerator = (colors: Swatch[]) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
"%cPicked colors",
|
||||
`color: ${foregroundColor}; background-color: ${backgroundColor.hex}; font-weight: var(--ha-font-weight-bold); padding: 16px;`
|
||||
`color: ${foregroundColor}; background-color: ${backgroundColor.hex}; font-weight: bold; padding: 16px;`
|
||||
);
|
||||
colors.forEach((color) => logColor(color));
|
||||
// eslint-disable-next-line no-console
|
||||
|
@@ -719,7 +719,7 @@ export class HaChartBase extends LitElement {
|
||||
max-height: 60%;
|
||||
overflow-y: auto;
|
||||
padding: 12px 0 0;
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-size: 12px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.chart-legend ul {
|
||||
|
@@ -106,6 +106,10 @@ export class HaSankeyChart extends LitElement {
|
||||
private _createData = memoizeOne((data: SankeyChartData, width = 0) => {
|
||||
const filteredNodes = data.nodes.filter((n) => n.value > 0);
|
||||
const indexes = [...new Set(filteredNodes.map((n) => n.index))];
|
||||
const depthMap = new Map<number, number>();
|
||||
indexes.sort().forEach((index, i) => {
|
||||
depthMap.set(index, i);
|
||||
});
|
||||
const links = this._processLinks(filteredNodes, data.links);
|
||||
const sectionWidth = width / indexes.length;
|
||||
const labelSpace = sectionWidth - NODE_SIZE - LABEL_DISTANCE;
|
||||
@@ -119,7 +123,7 @@ export class HaSankeyChart extends LitElement {
|
||||
itemStyle: {
|
||||
color: node.color,
|
||||
},
|
||||
depth: node.index,
|
||||
depth: depthMap.get(node.index),
|
||||
})),
|
||||
links,
|
||||
draggable: false,
|
||||
|
@@ -293,7 +293,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
.info {
|
||||
text-align: center;
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
line-height: 60px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
|
@@ -634,7 +634,7 @@ export class StatisticsChart extends LitElement {
|
||||
}
|
||||
.info {
|
||||
text-align: center;
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
line-height: 60px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
|
@@ -931,9 +931,9 @@ export class HaDataTable extends LitElement {
|
||||
font-family: Roboto, sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: var(--ha-font-size-s);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.0178571429em;
|
||||
text-decoration: inherit;
|
||||
text-transform: inherit;
|
||||
@@ -1051,9 +1051,9 @@ export class HaDataTable extends LitElement {
|
||||
font-family: Roboto, sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: var(--ha-font-size-s);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.0178571429em;
|
||||
text-decoration: inherit;
|
||||
text-transform: inherit;
|
||||
@@ -1173,9 +1173,9 @@ export class HaDataTable extends LitElement {
|
||||
font-family: Roboto, sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: var(--ha-font-size-s);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
font-weight: var(--ha-font-weight-semibold);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.375rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.0071428571em;
|
||||
text-decoration: inherit;
|
||||
text-transform: inherit;
|
||||
@@ -1199,7 +1199,7 @@ export class HaDataTable extends LitElement {
|
||||
padding-inline-start: 12px;
|
||||
padding-inline-end: initial;
|
||||
width: 100%;
|
||||
font-weight: var(--ha-font-weight-semibold);
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
@@ -4,11 +4,11 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { HaEntityComboBoxEntityFilterFunc } from "./ha-entity-combo-box";
|
||||
import "./ha-entity-picker";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
|
||||
|
||||
@customElement("ha-entities-picker")
|
||||
class HaEntitiesPickerLight extends LitElement {
|
||||
class HaEntitiesPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Array }) public value?: string[];
|
||||
@@ -17,6 +17,10 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
/**
|
||||
@@ -67,13 +71,8 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
@property({ type: Array, attribute: "exclude-entities" })
|
||||
public excludeEntities?: string[];
|
||||
|
||||
@property({ attribute: "picked-entity-label" })
|
||||
public pickedEntityLabel?: string;
|
||||
|
||||
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
public entityFilter?: HaEntityComboBoxEntityFilterFunc;
|
||||
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
|
||||
@@ -84,6 +83,7 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
|
||||
const currentEntities = this._currentEntities;
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
${currentEntities.map(
|
||||
(entityId) => html`
|
||||
<div>
|
||||
@@ -99,7 +99,6 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.value=${entityId}
|
||||
.label=${this.pickedEntityLabel}
|
||||
.disabled=${this.disabled}
|
||||
.createDomains=${this.createDomains}
|
||||
@value-changed=${this._entityChanged}
|
||||
@@ -121,7 +120,7 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.label=${this.pickEntityLabel}
|
||||
.placeholder=${this.placeholder}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.createDomains=${this.createDomains}
|
||||
@@ -198,11 +197,15 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
div {
|
||||
margin-top: 8px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-entities-picker": HaEntitiesPickerLight;
|
||||
"ha-entities-picker": HaEntitiesPicker;
|
||||
}
|
||||
}
|
||||
|
@@ -73,16 +73,20 @@ class HaEntityAttributePicker extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this.entityId!] as HassEntity | undefined;
|
||||
|
||||
return html`
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
.value=${this.value
|
||||
? computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
this.hass.states[this.entityId!],
|
||||
this.hass.entities,
|
||||
this.value
|
||||
)
|
||||
? stateObj
|
||||
? computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.value
|
||||
)
|
||||
: this.value
|
||||
: ""}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label ??
|
||||
|
539
src/components/entity/ha-entity-combo-box.ts
Normal file
539
src/components/entity/ha-entity-combo-box.ts
Normal file
@@ -0,0 +1,539 @@
|
||||
import { mdiMagnify, mdiPlus } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import Fuse from "fuse.js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import type { HelperDomain } from "../../panels/config/helpers/const";
|
||||
import { isHelperDomain } from "../../panels/config/helpers/const";
|
||||
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
|
||||
import { HaFuse } from "../../resources/fuse";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
|
||||
const FAKE_ENTITY: HassEntity = {
|
||||
entity_id: "",
|
||||
state: "",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "", user_id: null, parent_id: null },
|
||||
attributes: {},
|
||||
};
|
||||
|
||||
interface EntityComboBoxItem extends HassEntity {
|
||||
// Force empty label to always display empty value by default in the search field
|
||||
label: "";
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
translated_domain?: string;
|
||||
show_entity_id?: boolean;
|
||||
entity_name?: string;
|
||||
area_name?: string;
|
||||
device_name?: string;
|
||||
friendly_name?: string;
|
||||
sorting_label?: string;
|
||||
icon_path?: string;
|
||||
}
|
||||
|
||||
export type HaEntityComboBoxEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||
|
||||
const CREATE_ID = "___create-new-entity___";
|
||||
const NO_ENTITIES_ID = "___no-entities___";
|
||||
|
||||
const DOMAIN_STYLE = styleMap({
|
||||
fontSize: "var(--ha-font-size-s)",
|
||||
fontWeight: "var(--ha-font-weight-normal)",
|
||||
lineHeight: "var(--ha-line-height-normal)",
|
||||
alignSelf: "flex-end",
|
||||
maxWidth: "30%",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
});
|
||||
|
||||
const ENTITY_ID_STYLE = styleMap({
|
||||
fontFamily: "var(--ha-font-family-code)",
|
||||
fontSize: "var(--ha-font-size-xs)",
|
||||
});
|
||||
|
||||
@customElement("ha-entity-combo-box")
|
||||
export class HaEntityComboBox extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||
public allowCustomEntity;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
/**
|
||||
* Show only entities with these unit of measuments.
|
||||
* @type {Array}
|
||||
* @attr include-unit-of-measurement
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-unit-of-measurement" })
|
||||
public includeUnitOfMeasurement?: string[];
|
||||
|
||||
/**
|
||||
* List of allowed entities to show.
|
||||
* @type {Array}
|
||||
* @attr include-entities
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-entities" })
|
||||
public includeEntities?: string[];
|
||||
|
||||
/**
|
||||
* List of entities to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-entities
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-entities" })
|
||||
public excludeEntities?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: HaEntityComboBoxEntityFilterFunc;
|
||||
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.open();
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.focus();
|
||||
}
|
||||
|
||||
private _initialItems = false;
|
||||
|
||||
private _items: EntityComboBoxItem[] = [];
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.hass.loadBackendTranslation("title");
|
||||
}
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = (
|
||||
item,
|
||||
{ index }
|
||||
) => html`
|
||||
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
||||
${item.icon_path
|
||||
? html`<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>`
|
||||
: html`
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${item.entity_id && item.show_entity_id
|
||||
? html`<span slot="supporting-text" style=${ENTITY_ID_STYLE}
|
||||
>${item.entity_id}</span
|
||||
>`
|
||||
: nothing}
|
||||
${item.translated_domain && !item.show_entity_id
|
||||
? html`<div slot="trailing-supporting-text" style=${DOMAIN_STYLE}>
|
||||
${item.translated_domain}
|
||||
</div>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
_opened: boolean,
|
||||
hass: this["hass"],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
entityFilter: this["entityFilter"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
|
||||
includeEntities: this["includeEntities"],
|
||||
excludeEntities: this["excludeEntities"],
|
||||
createDomains: this["createDomains"]
|
||||
): EntityComboBoxItem[] => {
|
||||
let states: EntityComboBoxItem[] = [];
|
||||
|
||||
let entityIds = Object.keys(hass.states);
|
||||
|
||||
const createItems = createDomains?.length
|
||||
? createDomains.map((domain) => {
|
||||
const primary = hass.localize(
|
||||
"ui.components.entity.entity-picker.create_helper",
|
||||
{
|
||||
domain: isHelperDomain(domain)
|
||||
? hass.localize(
|
||||
`ui.panel.config.helpers.types.${domain as HelperDomain}`
|
||||
)
|
||||
: domainToName(hass.localize, domain),
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
...FAKE_ENTITY,
|
||||
label: "",
|
||||
entity_id: CREATE_ID + domain,
|
||||
primary: primary,
|
||||
secondary: this.hass.localize(
|
||||
"ui.components.entity.entity-picker.new_entity"
|
||||
),
|
||||
icon_path: mdiPlus,
|
||||
} satisfies EntityComboBoxItem;
|
||||
})
|
||||
: [];
|
||||
|
||||
if (!entityIds.length) {
|
||||
return [
|
||||
{
|
||||
...FAKE_ENTITY,
|
||||
label: "",
|
||||
entity_id: NO_ENTITIES_ID,
|
||||
primary: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_entities"
|
||||
),
|
||||
icon_path: mdiMagnify,
|
||||
},
|
||||
...createItems,
|
||||
];
|
||||
}
|
||||
|
||||
if (includeEntities) {
|
||||
entityIds = entityIds.filter((entityId) =>
|
||||
includeEntities.includes(entityId)
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeEntities) {
|
||||
entityIds = entityIds.filter(
|
||||
(entityId) => !excludeEntities.includes(entityId)
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDomains) {
|
||||
entityIds = entityIds.filter((eid) =>
|
||||
includeDomains.includes(computeDomain(eid))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
entityIds = entityIds.filter(
|
||||
(eid) => !excludeDomains.includes(computeDomain(eid))
|
||||
);
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
states = entityIds
|
||||
.map<EntityComboBoxItem>((entityId) => {
|
||||
const stateObj = hass!.states[entityId];
|
||||
|
||||
const { area, device } = getEntityContext(stateObj, hass);
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
const entityName = computeEntityName(stateObj, hass);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
const translatedDomain = domainToName(
|
||||
this.hass.localize,
|
||||
computeDomain(entityId)
|
||||
);
|
||||
|
||||
return {
|
||||
...hass!.states[entityId],
|
||||
label: "",
|
||||
primary: primary,
|
||||
secondary:
|
||||
secondary ||
|
||||
this.hass.localize("ui.components.device-picker.no_area"),
|
||||
translated_domain: translatedDomain,
|
||||
sorting_label: [deviceName, entityName].filter(Boolean).join("-"),
|
||||
entity_name: entityName || deviceName,
|
||||
area_name: areaName,
|
||||
device_name: deviceName,
|
||||
friendly_name: friendlyName,
|
||||
show_entity_id: hass.userData?.showEntityIdPicker,
|
||||
};
|
||||
})
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.sorting_label!,
|
||||
entityB.sorting_label!,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
states = states.filter(
|
||||
(stateObj) =>
|
||||
// We always want to include the entity of the current value
|
||||
stateObj.entity_id === this.value ||
|
||||
(stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class))
|
||||
);
|
||||
}
|
||||
|
||||
if (includeUnitOfMeasurement) {
|
||||
states = states.filter(
|
||||
(stateObj) =>
|
||||
// We always want to include the entity of the current value
|
||||
stateObj.entity_id === this.value ||
|
||||
(stateObj.attributes.unit_of_measurement &&
|
||||
includeUnitOfMeasurement.includes(
|
||||
stateObj.attributes.unit_of_measurement
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
states = states.filter(
|
||||
(stateObj) =>
|
||||
// We always want to include the entity of the current value
|
||||
stateObj.entity_id === this.value || entityFilter!(stateObj)
|
||||
);
|
||||
}
|
||||
|
||||
if (!states.length) {
|
||||
return [
|
||||
{
|
||||
...FAKE_ENTITY,
|
||||
label: "",
|
||||
entity_id: NO_ENTITIES_ID,
|
||||
primary: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_match"
|
||||
),
|
||||
icon_path: mdiMagnify,
|
||||
},
|
||||
...createItems,
|
||||
];
|
||||
}
|
||||
|
||||
if (createItems?.length) {
|
||||
states.push(...createItems);
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
);
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
changedProps.has("value") ||
|
||||
changedProps.has("label") ||
|
||||
changedProps.has("disabled")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return !(!changedProps.has("_opened") && this._opened);
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (!this._initialItems || (changedProps.has("_opened") && this._opened)) {
|
||||
this._items = this._getItems(
|
||||
this._opened,
|
||||
this.hass,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.entityFilter,
|
||||
this.includeDeviceClasses,
|
||||
this.includeUnitOfMeasurement,
|
||||
this.includeEntities,
|
||||
this.excludeEntities,
|
||||
this.createDomains
|
||||
);
|
||||
if (this._initialItems) {
|
||||
this.comboBox.filteredItems = this._items;
|
||||
}
|
||||
this._initialItems = true;
|
||||
}
|
||||
|
||||
if (changedProps.has("createDomains") && this.createDomains?.length) {
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
item-value-path="entity_id"
|
||||
.hass=${this.hass}
|
||||
.value=${this._value}
|
||||
.label=${this.label === undefined
|
||||
? this.hass.localize("ui.components.entity.entity-picker.entity")
|
||||
: this.label}
|
||||
.helper=${this.helper}
|
||||
.allowCustomValue=${this.allowCustomEntity}
|
||||
.filteredItems=${this._items}
|
||||
.renderer=${this._rowRenderer}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
>
|
||||
</ha-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
|
||||
ev.stopPropagation();
|
||||
// Clear the input field to prevent showing the old value next time
|
||||
this.comboBox.setTextFieldValue("");
|
||||
const newValue = ev.detail.value?.trim();
|
||||
|
||||
if (newValue && newValue.startsWith(CREATE_ID)) {
|
||||
const domain = newValue.substring(CREATE_ID.length);
|
||||
showHelperDetailDialog(this, {
|
||||
domain,
|
||||
dialogClosedCallback: (item) => {
|
||||
if (item.entityId) this._setValue(item.entityId);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private _fuseIndex = memoizeOne((states: EntityComboBoxItem[]) =>
|
||||
Fuse.createIndex(
|
||||
[
|
||||
"entity_name",
|
||||
"device_name",
|
||||
"area_name",
|
||||
"translated_domain",
|
||||
"friendly_name", // for backwards compatibility
|
||||
"entity_id", // for technical search
|
||||
],
|
||||
states
|
||||
)
|
||||
);
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
if (!this._opened) return;
|
||||
|
||||
const target = ev.target as HaComboBox;
|
||||
const filterString = ev.detail.value.trim().toLowerCase() as string;
|
||||
|
||||
const index = this._fuseIndex(this._items);
|
||||
const fuse = new HaFuse(this._items, {}, index);
|
||||
|
||||
const results = fuse.multiTermsSearch(filterString);
|
||||
if (results) {
|
||||
if (results.length === 0) {
|
||||
target.filteredItems = [
|
||||
{
|
||||
...FAKE_ENTITY,
|
||||
label: "",
|
||||
entity_id: NO_ENTITIES_ID,
|
||||
primary: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_match"
|
||||
),
|
||||
icon_path: mdiMagnify,
|
||||
},
|
||||
] as EntityComboBoxItem[];
|
||||
} else {
|
||||
target.filteredItems = results.map((result) => result.item);
|
||||
}
|
||||
} else {
|
||||
target.filteredItems = this._items;
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: string | undefined) {
|
||||
if (!value || !isValidEntityId(value)) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-entity-combo-box": HaEntityComboBox;
|
||||
}
|
||||
}
|
@@ -1,77 +1,27 @@
|
||||
import { mdiMagnify, mdiPlus } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import Fuse from "fuse.js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { mdiClose, mdiMenuDown, mdiShape } from "@mdi/js";
|
||||
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { getEntityContext } from "../../common/entity/get_entity_context";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import type { HelperDomain } from "../../panels/config/helpers/const";
|
||||
import { isHelperDomain } from "../../panels/config/helpers/const";
|
||||
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
|
||||
import { HaFuse } from "../../resources/fuse";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-icon-button";
|
||||
import type { HaMdListItem } from "../ha-md-list-item";
|
||||
import "../ha-svg-icon";
|
||||
import "./ha-entity-combo-box";
|
||||
import type {
|
||||
HaEntityComboBox,
|
||||
HaEntityComboBoxEntityFilterFunc,
|
||||
} from "./ha-entity-combo-box";
|
||||
import "./state-badge";
|
||||
|
||||
const FAKE_ENTITY: HassEntity = {
|
||||
entity_id: "",
|
||||
state: "",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "", user_id: null, parent_id: null },
|
||||
attributes: {},
|
||||
};
|
||||
|
||||
interface EntityPickerItem extends HassEntity {
|
||||
label: string;
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
translated_domain?: string;
|
||||
show_entity_id?: boolean;
|
||||
entity_name?: string;
|
||||
area_name?: string;
|
||||
device_name?: string;
|
||||
friendly_name?: string;
|
||||
sorting_label?: string;
|
||||
icon_path?: string;
|
||||
}
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||
|
||||
const CREATE_ID = "___create-new-entity___";
|
||||
|
||||
const DOMAIN_STYLE = styleMap({
|
||||
fontSize: "12px",
|
||||
fontWeight: "400",
|
||||
lineHeight: "18px",
|
||||
alignSelf: "flex-end",
|
||||
maxWidth: "30%",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
});
|
||||
|
||||
const ENTITY_ID_STYLE = styleMap({
|
||||
fontFamily: "var(--code-font-family, monospace)",
|
||||
fontSize: "11px",
|
||||
});
|
||||
|
||||
@customElement("ha-entity-picker")
|
||||
export class HaEntityPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -92,6 +42,8 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
@@ -143,381 +95,240 @@ export class HaEntityPicker extends LitElement {
|
||||
public excludeEntities?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
public entityFilter?: HaEntityComboBoxEntityFilterFunc;
|
||||
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
|
||||
@property({ attribute: "item-label-path" }) public itemLabelPath = "label";
|
||||
@query("#anchor") private _anchor?: HaMdListItem;
|
||||
|
||||
@query("#input") private _input?: HaEntityComboBox;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
private _renderContent() {
|
||||
const entityId = this.value || "";
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.open();
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.focus();
|
||||
}
|
||||
|
||||
private _initialItems = false;
|
||||
|
||||
private _items: EntityPickerItem[] = [];
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.hass.loadBackendTranslation("title");
|
||||
}
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<EntityPickerItem> = (
|
||||
item,
|
||||
{ index }
|
||||
) => html`
|
||||
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
||||
${item.icon_path
|
||||
? html`<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>`
|
||||
: html`
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`}
|
||||
|
||||
<span slot="headline">${item.primary} </span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${item.entity_id && item.show_entity_id
|
||||
? html`<span slot="supporting-text" style=${ENTITY_ID_STYLE}
|
||||
>${item.entity_id}</span
|
||||
>`
|
||||
: nothing}
|
||||
${item.translated_domain && !item.show_entity_id
|
||||
? html`<div slot="trailing-supporting-text" style=${DOMAIN_STYLE}>
|
||||
${item.translated_domain}
|
||||
</div>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
_opened: boolean,
|
||||
hass: this["hass"],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
entityFilter: this["entityFilter"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
|
||||
includeEntities: this["includeEntities"],
|
||||
excludeEntities: this["excludeEntities"],
|
||||
createDomains: this["createDomains"]
|
||||
): EntityPickerItem[] => {
|
||||
let states: EntityPickerItem[] = [];
|
||||
|
||||
if (!hass) {
|
||||
return [];
|
||||
}
|
||||
let entityIds = Object.keys(hass.states);
|
||||
|
||||
const createItems = createDomains?.length
|
||||
? createDomains.map((domain) => {
|
||||
const primary = hass.localize(
|
||||
"ui.components.entity.entity-picker.create_helper",
|
||||
{
|
||||
domain: isHelperDomain(domain)
|
||||
? hass.localize(
|
||||
`ui.panel.config.helpers.types.${domain as HelperDomain}`
|
||||
)
|
||||
: domainToName(hass.localize, domain),
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
...FAKE_ENTITY,
|
||||
entity_id: CREATE_ID + domain,
|
||||
primary: primary,
|
||||
label: primary,
|
||||
secondary: this.hass.localize(
|
||||
"ui.components.entity.entity-picker.new_entity"
|
||||
),
|
||||
icon_path: mdiPlus,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
if (!entityIds.length) {
|
||||
return [
|
||||
{
|
||||
...FAKE_ENTITY,
|
||||
primary: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_entities"
|
||||
),
|
||||
label: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_entities"
|
||||
),
|
||||
icon_path: mdiMagnify,
|
||||
},
|
||||
...createItems,
|
||||
];
|
||||
}
|
||||
|
||||
if (includeEntities) {
|
||||
entityIds = entityIds.filter((entityId) =>
|
||||
includeEntities.includes(entityId)
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeEntities) {
|
||||
entityIds = entityIds.filter(
|
||||
(entityId) => !excludeEntities.includes(entityId)
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDomains) {
|
||||
entityIds = entityIds.filter((eid) =>
|
||||
includeDomains.includes(computeDomain(eid))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
entityIds = entityIds.filter(
|
||||
(eid) => !excludeDomains.includes(computeDomain(eid))
|
||||
);
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
states = entityIds
|
||||
.map<EntityPickerItem>((entityId) => {
|
||||
const stateObj = hass!.states[entityId];
|
||||
|
||||
const { area, device } = getEntityContext(stateObj, hass);
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
const entityName = computeEntityName(stateObj, hass);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
const translatedDomain = domainToName(
|
||||
this.hass.localize,
|
||||
computeDomain(entityId)
|
||||
);
|
||||
|
||||
return {
|
||||
...hass!.states[entityId],
|
||||
primary: primary,
|
||||
secondary:
|
||||
secondary ||
|
||||
this.hass.localize("ui.components.device-picker.no_area"),
|
||||
label: friendlyName,
|
||||
translated_domain: translatedDomain,
|
||||
sorting_label: [deviceName, entityName].filter(Boolean).join("-"),
|
||||
entity_name: entityName || deviceName,
|
||||
area_name: areaName,
|
||||
device_name: deviceName,
|
||||
friendly_name: friendlyName,
|
||||
show_entity_id: hass.userData?.showEntityIdPicker,
|
||||
};
|
||||
})
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.sorting_label!,
|
||||
entityB.sorting_label!,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
states = states.filter(
|
||||
(stateObj) =>
|
||||
// We always want to include the entity of the current value
|
||||
stateObj.entity_id === this.value ||
|
||||
(stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class))
|
||||
);
|
||||
}
|
||||
|
||||
if (includeUnitOfMeasurement) {
|
||||
states = states.filter(
|
||||
(stateObj) =>
|
||||
// We always want to include the entity of the current value
|
||||
stateObj.entity_id === this.value ||
|
||||
(stateObj.attributes.unit_of_measurement &&
|
||||
includeUnitOfMeasurement.includes(
|
||||
stateObj.attributes.unit_of_measurement
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
states = states.filter(
|
||||
(stateObj) =>
|
||||
// We always want to include the entity of the current value
|
||||
stateObj.entity_id === this.value || entityFilter!(stateObj)
|
||||
);
|
||||
}
|
||||
|
||||
if (!states.length) {
|
||||
return [
|
||||
{
|
||||
...FAKE_ENTITY,
|
||||
primary: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_match"
|
||||
),
|
||||
label: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_match"
|
||||
),
|
||||
icon_path: mdiMagnify,
|
||||
},
|
||||
...createItems,
|
||||
];
|
||||
}
|
||||
|
||||
if (createItems?.length) {
|
||||
states.push(...createItems);
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
);
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
changedProps.has("value") ||
|
||||
changedProps.has("label") ||
|
||||
changedProps.has("disabled")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return !(!changedProps.has("_opened") && this._opened);
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (!this._initialItems || (changedProps.has("_opened") && this._opened)) {
|
||||
this._items = this._getItems(
|
||||
this._opened,
|
||||
this.hass,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.entityFilter,
|
||||
this.includeDeviceClasses,
|
||||
this.includeUnitOfMeasurement,
|
||||
this.includeEntities,
|
||||
this.excludeEntities,
|
||||
this.createDomains
|
||||
);
|
||||
if (this._initialItems) {
|
||||
this.comboBox.filteredItems = this._items;
|
||||
}
|
||||
this._initialItems = true;
|
||||
if (!this.value) {
|
||||
return html`
|
||||
<span slot="headline" class="placeholder"
|
||||
>${this.placeholder ??
|
||||
this.hass.localize(
|
||||
"ui.components.entity.entity-picker.placeholder"
|
||||
)}</span
|
||||
>
|
||||
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
|
||||
`;
|
||||
}
|
||||
|
||||
if (changedProps.has("createDomains") && this.createDomains?.length) {
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
}
|
||||
}
|
||||
const stateObj = this.hass.states[entityId];
|
||||
|
||||
const showClearIcon =
|
||||
!this.required && !this.disabled && !this.hideClearIcon;
|
||||
|
||||
if (!stateObj) {
|
||||
return html`
|
||||
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
|
||||
<span slot="headline">${entityId}</span>
|
||||
${showClearIcon
|
||||
? html`<ha-icon-button
|
||||
class="clear"
|
||||
slot="end"
|
||||
@click=${this._clear}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
|
||||
`;
|
||||
}
|
||||
|
||||
const { area, device } = getEntityContext(stateObj, this.hass);
|
||||
|
||||
const entityName = computeEntityName(stateObj, this.hass);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
item-value-path="entity_id"
|
||||
.itemLabelPath=${this.itemLabelPath}
|
||||
<state-badge
|
||||
.hass=${this.hass}
|
||||
.value=${this._value}
|
||||
.label=${this.label === undefined
|
||||
? this.hass.localize("ui.components.entity.entity-picker.entity")
|
||||
: this.label}
|
||||
.helper=${this.helper}
|
||||
.allowCustomValue=${this.allowCustomEntity}
|
||||
.filteredItems=${this._items}
|
||||
.renderer=${this._rowRenderer}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
>
|
||||
</ha-combo-box>
|
||||
.stateObj=${stateObj}
|
||||
slot="start"
|
||||
></state-badge>
|
||||
<span slot="headline">${primary}</span>
|
||||
<span slot="supporting-text">
|
||||
${secondary ||
|
||||
this.hass.localize("ui.components.device-picker.no_area")}
|
||||
</span>
|
||||
${showClearIcon
|
||||
? html`<ha-icon-button
|
||||
class="clear"
|
||||
slot="end"
|
||||
@click=${this._clear}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
protected render() {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<div class="container">
|
||||
${!this._opened
|
||||
? html`<ha-combo-box-item
|
||||
.disabled=${this.disabled}
|
||||
id="anchor"
|
||||
type="button"
|
||||
compact
|
||||
@click=${this._showPicker}
|
||||
>
|
||||
${this._renderContent()}
|
||||
</ha-combo-box-item>`
|
||||
: html`<ha-entity-combo-box
|
||||
id="input"
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.allowCustomEntity=${this.allowCustomEntity}
|
||||
.label=${this.hass.localize("ui.common.search")}
|
||||
.value=${this.value}
|
||||
.createDomains=${this.createDomains}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.includeEntities=${this.includeEntities}
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.entityFilter=${this.entityFilter}
|
||||
hide-clear-icon
|
||||
@opened-changed=${this._debounceOpenedChanged}
|
||||
@input=${stopPropagation}
|
||||
></ha-entity-combo-box>`}
|
||||
${this._renderHelper()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value?.trim();
|
||||
private _clear(e) {
|
||||
e.stopPropagation();
|
||||
this.value = undefined;
|
||||
fireEvent(this, "value-changed", { value: undefined });
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
if (newValue && newValue.startsWith(CREATE_ID)) {
|
||||
const domain = newValue.substring(CREATE_ID.length);
|
||||
showHelperDetailDialog(this, {
|
||||
domain,
|
||||
dialogClosedCallback: (item) => {
|
||||
if (item.entityId) this._setValue(item.entityId);
|
||||
},
|
||||
});
|
||||
private async _showPicker() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
this._opened = true;
|
||||
await this.updateComplete;
|
||||
this._input?.focus();
|
||||
this._input?.open();
|
||||
}
|
||||
|
||||
private _fuseIndex = memoizeOne((states: EntityPickerItem[]) =>
|
||||
Fuse.createIndex(
|
||||
[
|
||||
"entity_name",
|
||||
"device_name",
|
||||
"area_name",
|
||||
"translated_domain",
|
||||
"friendly_name", // for backwards compatibility
|
||||
"entity_id", // for technical search
|
||||
],
|
||||
states
|
||||
)
|
||||
// Multiple calls to _openedChanged can be triggered in quick succession
|
||||
// when the menu is opened
|
||||
private _debounceOpenedChanged = debounce(
|
||||
(ev) => this._openedChanged(ev),
|
||||
10
|
||||
);
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
if (!this._opened) return;
|
||||
|
||||
const target = ev.target as HaComboBox;
|
||||
const filterString = ev.detail.value.trim().toLowerCase() as string;
|
||||
|
||||
const index = this._fuseIndex(this._items);
|
||||
const fuse = new HaFuse(this._items, {}, index);
|
||||
|
||||
const results = fuse.multiTermsSearch(filterString);
|
||||
if (results) {
|
||||
target.filteredItems = results.map((result) => result.item);
|
||||
} else {
|
||||
target.filteredItems = this._items;
|
||||
private async _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
|
||||
const opened = ev.detail.value;
|
||||
if (this._opened && !opened) {
|
||||
this._opened = false;
|
||||
await this.updateComplete;
|
||||
this._anchor?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: string | undefined) {
|
||||
this.value = value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
mwc-menu-surface {
|
||||
--mdc-menu-min-width: 100%;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
ha-combo-box-item {
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-radius: 4px;
|
||||
border-end-end-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
--md-list-item-one-line-container-height: 56px;
|
||||
--md-list-item-two-line-container-height: 56px;
|
||||
--md-list-item-top-space: 8px;
|
||||
--md-list-item-bottom-space: 8px;
|
||||
--md-list-item-leading-space: 8px;
|
||||
--md-list-item-trailing-space: 8px;
|
||||
--ha-md-list-item-gap: 8px;
|
||||
/* Remove the default focus ring */
|
||||
--md-focus-ring-width: 0px;
|
||||
--md-focus-ring-duration: 0s;
|
||||
}
|
||||
|
||||
/* Add Similar focus style as the text field */
|
||||
ha-combo-box-item:after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: var(
|
||||
--mdc-text-field-idle-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
transform:
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
}
|
||||
|
||||
ha-combo-box-item:focus:after {
|
||||
height: 2px;
|
||||
background-color: var(--mdc-theme-primary);
|
||||
}
|
||||
|
||||
ha-combo-box-item ha-svg-icon[slot="start"] {
|
||||
margin: 0 4px;
|
||||
}
|
||||
.clear {
|
||||
margin: 0 -8px;
|
||||
--mdc-icon-button-size: 32px;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
.edit {
|
||||
--mdc-icon-size: 20px;
|
||||
width: 32px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.placeholder {
|
||||
color: var(--secondary-text-color);
|
||||
padding: 0 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
482
src/components/entity/ha-statistic-combo-box.ts
Normal file
482
src/components/entity/ha-statistic-combo-box.ts
Normal file
@@ -0,0 +1,482 @@
|
||||
import { mdiChartLine, mdiShape } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import Fuse from "fuse.js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import type { StatisticsMetaData } from "../../data/recorder";
|
||||
import { getStatisticIds, getStatisticLabel } from "../../data/recorder";
|
||||
import { HaFuse } from "../../resources/fuse";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
|
||||
type StatisticItemType = "entity" | "external" | "no_state";
|
||||
|
||||
interface StatisticItem {
|
||||
id: string;
|
||||
label: "";
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
show_entity_id?: boolean;
|
||||
entity_name?: string;
|
||||
area_name?: string;
|
||||
device_name?: string;
|
||||
friendly_name?: string;
|
||||
sorting_label?: string;
|
||||
state?: HassEntity;
|
||||
type?: StatisticItemType;
|
||||
iconPath?: string;
|
||||
}
|
||||
|
||||
const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[];
|
||||
|
||||
const ENTITY_ID_STYLE = styleMap({
|
||||
fontFamily: "var(--ha-font-family-code)",
|
||||
fontSize: "11px",
|
||||
});
|
||||
|
||||
@customElement("ha-statistic-combo-box")
|
||||
export class HaStatisticComboBox extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property({ attribute: "statistic-types" })
|
||||
public statisticTypes?: "mean" | "sum";
|
||||
|
||||
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||
public allowCustomEntity;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
public statisticIds?: StatisticsMetaData[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
/**
|
||||
* Show only statistics natively stored with these units of measurements.
|
||||
* @type {Array}
|
||||
* @attr include-statistics-unit-of-measurement
|
||||
*/
|
||||
@property({
|
||||
type: Array,
|
||||
attribute: "include-statistics-unit-of-measurement",
|
||||
})
|
||||
public includeStatisticsUnitOfMeasurement?: string | string[];
|
||||
|
||||
/**
|
||||
* Show only statistics with these unit classes.
|
||||
* @attr include-unit-class
|
||||
*/
|
||||
@property({ attribute: "include-unit-class" })
|
||||
public includeUnitClass?: string | string[];
|
||||
|
||||
/**
|
||||
* Show only statistics with these device classes.
|
||||
* @attr include-device-class
|
||||
*/
|
||||
@property({ attribute: "include-device-class" })
|
||||
public includeDeviceClass?: string | string[];
|
||||
|
||||
/**
|
||||
* Show only statistics on entities.
|
||||
* @type {Boolean}
|
||||
* @attr entities-only
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "entities-only" })
|
||||
public entitiesOnly = false;
|
||||
|
||||
/**
|
||||
* List of statistics to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-statistics
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-statistics" })
|
||||
public excludeStatistics?: string[];
|
||||
|
||||
@property({ attribute: false }) public helpMissingEntityUrl =
|
||||
"/more-info/statistics/";
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
|
||||
private _initialItems = false;
|
||||
|
||||
private _items: StatisticItem[] = [];
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.hass.loadBackendTranslation("title");
|
||||
}
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = (
|
||||
item,
|
||||
{ index }
|
||||
) => html`
|
||||
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
||||
${!item.state
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
style="margin: 0 4px"
|
||||
slot="start"
|
||||
.path=${item.iconPath}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: html`
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item.state}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`}
|
||||
|
||||
<span slot="headline">${item.primary} </span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${item.id && item.show_entity_id
|
||||
? html`
|
||||
<span slot="supporting-text" style=${ENTITY_ID_STYLE}>
|
||||
${item.id}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
_opened: boolean,
|
||||
hass: this["hass"],
|
||||
statisticIds: StatisticsMetaData[],
|
||||
includeStatisticsUnitOfMeasurement?: string | string[],
|
||||
includeUnitClass?: string | string[],
|
||||
includeDeviceClass?: string | string[],
|
||||
entitiesOnly?: boolean,
|
||||
excludeStatistics?: string[],
|
||||
value?: string
|
||||
): StatisticItem[] => {
|
||||
if (!statisticIds.length) {
|
||||
return [
|
||||
{
|
||||
id: "",
|
||||
label: "",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.statistic-picker.no_statistics"
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (includeStatisticsUnitOfMeasurement) {
|
||||
const includeUnits: (string | null)[] = ensureArray(
|
||||
includeStatisticsUnitOfMeasurement
|
||||
);
|
||||
statisticIds = statisticIds.filter((meta) =>
|
||||
includeUnits.includes(meta.statistics_unit_of_measurement)
|
||||
);
|
||||
}
|
||||
if (includeUnitClass) {
|
||||
const includeUnitClasses: (string | null)[] =
|
||||
ensureArray(includeUnitClass);
|
||||
statisticIds = statisticIds.filter((meta) =>
|
||||
includeUnitClasses.includes(meta.unit_class)
|
||||
);
|
||||
}
|
||||
if (includeDeviceClass) {
|
||||
const includeDeviceClasses: (string | null)[] =
|
||||
ensureArray(includeDeviceClass);
|
||||
statisticIds = statisticIds.filter((meta) => {
|
||||
const stateObj = this.hass.states[meta.statistic_id];
|
||||
if (!stateObj) {
|
||||
return true;
|
||||
}
|
||||
return includeDeviceClasses.includes(
|
||||
stateObj.attributes.device_class || ""
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
const output: StatisticItem[] = [];
|
||||
statisticIds.forEach((meta) => {
|
||||
if (
|
||||
excludeStatistics &&
|
||||
meta.statistic_id !== value &&
|
||||
excludeStatistics.includes(meta.statistic_id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass.states[meta.statistic_id];
|
||||
|
||||
if (!stateObj) {
|
||||
if (!entitiesOnly) {
|
||||
const id = meta.statistic_id;
|
||||
const label = getStatisticLabel(this.hass, meta.statistic_id, meta);
|
||||
const type =
|
||||
meta.statistic_id.includes(":") &&
|
||||
!meta.statistic_id.includes(".")
|
||||
? "external"
|
||||
: "no_state";
|
||||
|
||||
if (type === "no_state") {
|
||||
output.push({
|
||||
id,
|
||||
primary: label,
|
||||
secondary: this.hass.localize(
|
||||
"ui.components.statistic-picker.no_state"
|
||||
),
|
||||
label: "",
|
||||
type,
|
||||
sorting_label: label,
|
||||
iconPath: mdiShape,
|
||||
});
|
||||
} else if (type === "external") {
|
||||
const domain = id.split(":")[0];
|
||||
const domainName = domainToName(this.hass.localize, domain);
|
||||
output.push({
|
||||
id,
|
||||
primary: label,
|
||||
secondary: domainName,
|
||||
label: "",
|
||||
type,
|
||||
sorting_label: label,
|
||||
iconPath: mdiChartLine,
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const id = meta.statistic_id;
|
||||
|
||||
const { area, device } = getEntityContext(stateObj, hass);
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
const entityName = computeEntityName(stateObj, hass);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
|
||||
const primary = entityName || deviceName || id;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
output.push({
|
||||
id,
|
||||
primary,
|
||||
secondary,
|
||||
label: "",
|
||||
state: stateObj,
|
||||
type: "entity",
|
||||
sorting_label: [deviceName, entityName].join("_"),
|
||||
entity_name: entityName || deviceName,
|
||||
area_name: areaName,
|
||||
device_name: deviceName,
|
||||
friendly_name: friendlyName,
|
||||
show_entity_id: hass.userData?.showEntityIdPicker,
|
||||
});
|
||||
});
|
||||
|
||||
if (!output.length) {
|
||||
return [
|
||||
{
|
||||
id: "",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.statistic-picker.no_match"
|
||||
),
|
||||
label: "",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (output.length > 1) {
|
||||
output.sort((a, b) => {
|
||||
const aPrefix = TYPE_ORDER.indexOf(a.type || "no_state");
|
||||
const bPrefix = TYPE_ORDER.indexOf(b.type || "no_state");
|
||||
|
||||
return caseInsensitiveStringCompare(
|
||||
`${aPrefix}_${a.sorting_label || ""}`,
|
||||
`${bPrefix}_${b.sorting_label || ""}`,
|
||||
this.hass.locale.language
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
output.push({
|
||||
id: "__missing",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.statistic-picker.missing_entity"
|
||||
),
|
||||
label: "",
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
);
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.open();
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.focus();
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
changedProps.has("value") ||
|
||||
changedProps.has("label") ||
|
||||
changedProps.has("disabled")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return !(!changedProps.has("_opened") && this._opened);
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this.hasUpdated && !this.statisticIds) ||
|
||||
changedProps.has("statisticTypes")
|
||||
) {
|
||||
this._getStatisticIds();
|
||||
}
|
||||
|
||||
if (
|
||||
this.statisticIds &&
|
||||
(!this._initialItems || (changedProps.has("_opened") && this._opened))
|
||||
) {
|
||||
this._items = this._getItems(
|
||||
this._opened,
|
||||
this.hass,
|
||||
this.statisticIds!,
|
||||
this.includeStatisticsUnitOfMeasurement,
|
||||
this.includeUnitClass,
|
||||
this.includeDeviceClass,
|
||||
this.entitiesOnly,
|
||||
this.excludeStatistics,
|
||||
this.value
|
||||
);
|
||||
if (this._initialItems) {
|
||||
this.comboBox.filteredItems = this._items;
|
||||
}
|
||||
this._initialItems = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (this._items.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.statistic-picker.statistic")
|
||||
: this.label}
|
||||
.value=${this._value}
|
||||
.renderer=${this._rowRenderer}
|
||||
.disabled=${this.disabled}
|
||||
.allowCustomValue=${this.allowCustomEntity}
|
||||
.filteredItems=${this._items}
|
||||
item-value-path="id"
|
||||
item-id-path="id"
|
||||
item-label-path="label"
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._statisticChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
></ha-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _getStatisticIds() {
|
||||
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _statisticChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
let newValue = ev.detail.value;
|
||||
if (newValue === "__missing") {
|
||||
newValue = "";
|
||||
}
|
||||
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
}
|
||||
|
||||
private _fuseIndex = memoizeOne((states: StatisticItem[]) =>
|
||||
Fuse.createIndex(
|
||||
[
|
||||
"entity_name",
|
||||
"device_name",
|
||||
"area_name",
|
||||
"friendly_name", // for backwards compatibility
|
||||
"id", // for technical search
|
||||
],
|
||||
states
|
||||
)
|
||||
);
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
if (!this._opened) return;
|
||||
|
||||
const target = ev.target as HaComboBox;
|
||||
const filterString = ev.detail.value.trim().toLowerCase() as string;
|
||||
|
||||
const index = this._fuseIndex(this._items);
|
||||
const fuse = new HaFuse(this._items, {}, index);
|
||||
|
||||
const results = fuse.multiTermsSearch(filterString);
|
||||
|
||||
if (results) {
|
||||
target.filteredItems = results.map((result) => result.item);
|
||||
} else {
|
||||
target.filteredItems = this._items;
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: string) {
|
||||
this.value = value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-statistic-combo-box": HaStatisticComboBox;
|
||||
}
|
||||
}
|
@@ -1,65 +1,66 @@
|
||||
import { mdiChartLine } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import Fuse from "fuse.js";
|
||||
import { mdiChartLine, mdiClose, mdiMenuDown, mdiShape } from "@mdi/js";
|
||||
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type CSSResultGroup,
|
||||
type PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { getEntityContext } from "../../common/entity/get_entity_context";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import type { StatisticsMetaData } from "../../data/recorder";
|
||||
import { getStatisticIds, getStatisticLabel } from "../../data/recorder";
|
||||
import { HaFuse } from "../../resources/fuse";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import {
|
||||
getStatisticIds,
|
||||
getStatisticLabel,
|
||||
type StatisticsMetaData,
|
||||
} from "../../data/recorder";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-icon-button";
|
||||
import type { HaMdListItem } from "../ha-md-list-item";
|
||||
import "../ha-svg-icon";
|
||||
import "./ha-entity-combo-box";
|
||||
import type { HaEntityComboBox } from "./ha-entity-combo-box";
|
||||
import "./ha-statistic-combo-box";
|
||||
import "./state-badge";
|
||||
|
||||
type StatisticItemType = "entity" | "external" | "no_state";
|
||||
|
||||
interface StatisticItem {
|
||||
id: string;
|
||||
label: string;
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
show_entity_id?: boolean;
|
||||
entity_name?: string;
|
||||
area_name?: string;
|
||||
device_name?: string;
|
||||
friendly_name?: string;
|
||||
sorting_label?: string;
|
||||
state?: HassEntity;
|
||||
type?: StatisticItemType;
|
||||
iconPath?: string;
|
||||
stateObj?: HassEntity;
|
||||
}
|
||||
|
||||
const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[];
|
||||
|
||||
const ENTITY_ID_STYLE = styleMap({
|
||||
fontFamily: "var(--code-font-family, monospace)",
|
||||
fontSize: "11px",
|
||||
});
|
||||
|
||||
@customElement("ha-statistic-picker")
|
||||
export class HaStatisticPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ attribute: "statistic-types" })
|
||||
public statisticTypes?: "mean" | "sum";
|
||||
|
||||
@@ -69,8 +70,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
@property({ attribute: false, type: Array })
|
||||
public statisticIds?: StatisticsMetaData[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
/**
|
||||
* Show only statistics natively stored with these units of measurements.
|
||||
* @type {Array}
|
||||
@@ -112,251 +111,15 @@ export class HaStatisticPicker extends LitElement {
|
||||
@property({ type: Array, attribute: "exclude-statistics" })
|
||||
public excludeStatistics?: string[];
|
||||
|
||||
@property({ attribute: false }) public helpMissingEntityUrl =
|
||||
"/more-info/statistics/";
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
|
||||
@query("#anchor") private _anchor?: HaMdListItem;
|
||||
|
||||
@query("#input") private _input?: HaEntityComboBox;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
|
||||
private _initialItems = false;
|
||||
|
||||
private _items: StatisticItem[] = [];
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.hass.loadBackendTranslation("title");
|
||||
}
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = (
|
||||
item,
|
||||
{ index }
|
||||
) => html`
|
||||
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
||||
${!item.state
|
||||
? html`<ha-svg-icon
|
||||
style="margin: 0 4px"
|
||||
slot="start"
|
||||
.path=${item.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: html`
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item.state}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`}
|
||||
|
||||
<span slot="headline">${item.primary} </span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${item.id && item.show_entity_id
|
||||
? html`
|
||||
<span slot="supporting-text" style=${ENTITY_ID_STYLE}>
|
||||
${item.id}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
_opened: boolean,
|
||||
hass: this["hass"],
|
||||
statisticIds: StatisticsMetaData[],
|
||||
includeStatisticsUnitOfMeasurement?: string | string[],
|
||||
includeUnitClass?: string | string[],
|
||||
includeDeviceClass?: string | string[],
|
||||
entitiesOnly?: boolean,
|
||||
excludeStatistics?: string[],
|
||||
value?: string
|
||||
): StatisticItem[] => {
|
||||
if (!statisticIds.length) {
|
||||
return [
|
||||
{
|
||||
id: "",
|
||||
label: this.hass.localize(
|
||||
"ui.components.statistic-picker.no_statistics"
|
||||
),
|
||||
primary: this.hass.localize(
|
||||
"ui.components.statistic-picker.no_statistics"
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (includeStatisticsUnitOfMeasurement) {
|
||||
const includeUnits: (string | null)[] = ensureArray(
|
||||
includeStatisticsUnitOfMeasurement
|
||||
);
|
||||
statisticIds = statisticIds.filter((meta) =>
|
||||
includeUnits.includes(meta.statistics_unit_of_measurement)
|
||||
);
|
||||
}
|
||||
if (includeUnitClass) {
|
||||
const includeUnitClasses: (string | null)[] =
|
||||
ensureArray(includeUnitClass);
|
||||
statisticIds = statisticIds.filter((meta) =>
|
||||
includeUnitClasses.includes(meta.unit_class)
|
||||
);
|
||||
}
|
||||
if (includeDeviceClass) {
|
||||
const includeDeviceClasses: (string | null)[] =
|
||||
ensureArray(includeDeviceClass);
|
||||
statisticIds = statisticIds.filter((meta) => {
|
||||
const stateObj = this.hass.states[meta.statistic_id];
|
||||
if (!stateObj) {
|
||||
return true;
|
||||
}
|
||||
return includeDeviceClasses.includes(
|
||||
stateObj.attributes.device_class || ""
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
const output: StatisticItem[] = [];
|
||||
statisticIds.forEach((meta) => {
|
||||
if (
|
||||
excludeStatistics &&
|
||||
meta.statistic_id !== value &&
|
||||
excludeStatistics.includes(meta.statistic_id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const entityState = this.hass.states[meta.statistic_id];
|
||||
if (!entityState) {
|
||||
if (!entitiesOnly) {
|
||||
const id = meta.statistic_id;
|
||||
const label = getStatisticLabel(this.hass, meta.statistic_id, meta);
|
||||
const type =
|
||||
meta.statistic_id.includes(":") &&
|
||||
!meta.statistic_id.includes(".")
|
||||
? "external"
|
||||
: "no_state";
|
||||
|
||||
if (type === "no_state") {
|
||||
output.push({
|
||||
id,
|
||||
primary: label,
|
||||
secondary: this.hass.localize(
|
||||
"ui.components.statistic-picker.no_state"
|
||||
),
|
||||
label,
|
||||
type,
|
||||
sorting_label: label,
|
||||
});
|
||||
} else if (type === "external") {
|
||||
const domain = id.split(":")[0];
|
||||
const domainName = domainToName(this.hass.localize, domain);
|
||||
output.push({
|
||||
id,
|
||||
primary: label,
|
||||
secondary: domainName,
|
||||
label,
|
||||
type,
|
||||
sorting_label: label,
|
||||
iconPath: mdiChartLine,
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const id = meta.statistic_id;
|
||||
|
||||
const { area, device } = getEntityContext(entityState, hass);
|
||||
|
||||
const friendlyName = computeStateName(entityState); // Keep this for search
|
||||
const entityName = computeEntityName(entityState, hass);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
|
||||
const primary = entityName || deviceName || id;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
output.push({
|
||||
id,
|
||||
primary,
|
||||
secondary,
|
||||
label: friendlyName,
|
||||
state: entityState,
|
||||
type: "entity",
|
||||
sorting_label: [deviceName, entityName].join("_"),
|
||||
entity_name: entityName || deviceName,
|
||||
area_name: areaName,
|
||||
device_name: deviceName,
|
||||
friendly_name: friendlyName,
|
||||
show_entity_id: hass.userData?.showEntityIdPicker,
|
||||
});
|
||||
});
|
||||
|
||||
if (!output.length) {
|
||||
return [
|
||||
{
|
||||
id: "",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.statistic-picker.no_match"
|
||||
),
|
||||
label: this.hass.localize(
|
||||
"ui.components.statistic-picker.no_match"
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (output.length > 1) {
|
||||
output.sort((a, b) => {
|
||||
const aPrefix = TYPE_ORDER.indexOf(a.type || "no_state");
|
||||
const bPrefix = TYPE_ORDER.indexOf(b.type || "no_state");
|
||||
|
||||
return caseInsensitiveStringCompare(
|
||||
`${aPrefix}_${a.sorting_label || ""}`,
|
||||
`${bPrefix}_${b.sorting_label || ""}`,
|
||||
this.hass.locale.language
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
output.push({
|
||||
id: "__missing",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.statistic-picker.missing_entity"
|
||||
),
|
||||
label: this.hass.localize(
|
||||
"ui.components.statistic-picker.missing_entity"
|
||||
),
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
);
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.open();
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.focus();
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
changedProps.has("value") ||
|
||||
changedProps.has("label") ||
|
||||
changedProps.has("disabled")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return !(!changedProps.has("_opened") && this._opened);
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this.hasUpdated && !this.statisticIds) ||
|
||||
@@ -364,117 +127,278 @@ export class HaStatisticPicker extends LitElement {
|
||||
) {
|
||||
this._getStatisticIds();
|
||||
}
|
||||
|
||||
if (
|
||||
this.statisticIds &&
|
||||
(!this._initialItems || (changedProps.has("_opened") && this._opened))
|
||||
) {
|
||||
this._items = this._getItems(
|
||||
this._opened,
|
||||
this.hass,
|
||||
this.statisticIds!,
|
||||
this.includeStatisticsUnitOfMeasurement,
|
||||
this.includeUnitClass,
|
||||
this.includeDeviceClass,
|
||||
this.entitiesOnly,
|
||||
this.excludeStatistics,
|
||||
this.value
|
||||
);
|
||||
if (this._initialItems) {
|
||||
this.comboBox.filteredItems = this._items;
|
||||
}
|
||||
this._initialItems = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (this._items.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.statistic-picker.statistic")
|
||||
: this.label}
|
||||
.value=${this._value}
|
||||
.renderer=${this._rowRenderer}
|
||||
.disabled=${this.disabled}
|
||||
.allowCustomValue=${this.allowCustomEntity}
|
||||
.filteredItems=${this._items}
|
||||
item-value-path="id"
|
||||
item-id-path="id"
|
||||
item-label-path="label"
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._statisticChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
></ha-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _getStatisticIds() {
|
||||
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _statisticChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
let newValue = ev.detail.value;
|
||||
if (newValue === "__missing") {
|
||||
newValue = "";
|
||||
private _statisticMetaData = memoizeOne(
|
||||
(statisticId: string, statisticIds: StatisticsMetaData[]) => {
|
||||
if (!statisticIds) {
|
||||
return undefined;
|
||||
}
|
||||
return statisticIds.find(
|
||||
(statistic) => statistic.statistic_id === statisticId
|
||||
);
|
||||
}
|
||||
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
}
|
||||
|
||||
private _fuseIndex = memoizeOne((states: StatisticItem[]) =>
|
||||
Fuse.createIndex(
|
||||
[
|
||||
"label",
|
||||
"entity_name",
|
||||
"device_name",
|
||||
"area_name",
|
||||
"friendly_name", // for backwards compatibility
|
||||
"id", // for technical search
|
||||
],
|
||||
states
|
||||
)
|
||||
);
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
if (!this._opened) return;
|
||||
private _renderContent() {
|
||||
const statisticId = this.value || "";
|
||||
|
||||
const target = ev.target as HaComboBox;
|
||||
const filterString = ev.detail.value.trim().toLowerCase() as string;
|
||||
if (!this.value) {
|
||||
return html`
|
||||
<span slot="headline" class="placeholder"
|
||||
>${this.placeholder ??
|
||||
this.hass.localize(
|
||||
"ui.components.statistic-picker.placeholder"
|
||||
)}</span
|
||||
>
|
||||
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
|
||||
`;
|
||||
}
|
||||
|
||||
const index = this._fuseIndex(this._items);
|
||||
const fuse = new HaFuse(this._items, {}, index);
|
||||
const item = this._computeItem(statisticId);
|
||||
|
||||
const results = fuse.multiTermsSearch(filterString);
|
||||
const showClearIcon =
|
||||
!this.required && !this.disabled && !this.hideClearIcon;
|
||||
|
||||
if (results) {
|
||||
target.filteredItems = results.map((result) => result.item);
|
||||
} else {
|
||||
target.filteredItems = this._items;
|
||||
return html`
|
||||
${item.stateObj
|
||||
? html`
|
||||
<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${item.stateObj}
|
||||
slot="start"
|
||||
></state-badge>
|
||||
`
|
||||
: item.iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${showClearIcon
|
||||
? html`<ha-icon-button
|
||||
class="clear"
|
||||
slot="end"
|
||||
@click=${this._clear}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeItem(statisticId: string): StatisticItem {
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
|
||||
if (stateObj) {
|
||||
const { area, device } = getEntityContext(stateObj, this.hass);
|
||||
|
||||
const entityName = computeEntityName(stateObj, this.hass);
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
const primary = entityName || deviceName || statisticId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
return {
|
||||
primary,
|
||||
secondary,
|
||||
stateObj,
|
||||
};
|
||||
}
|
||||
|
||||
const statistic = this.statisticIds
|
||||
? this._statisticMetaData(statisticId, this.statisticIds)
|
||||
: undefined;
|
||||
|
||||
if (statistic) {
|
||||
const type =
|
||||
statisticId.includes(":") && !statisticId.includes(".")
|
||||
? "external"
|
||||
: "no_state";
|
||||
|
||||
if (type === "external") {
|
||||
const label = getStatisticLabel(this.hass, statisticId, statistic);
|
||||
const domain = statisticId.split(":")[0];
|
||||
const domainName = domainToName(this.hass.localize, domain);
|
||||
|
||||
return {
|
||||
primary: label,
|
||||
secondary: domainName,
|
||||
iconPath: mdiChartLine,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
primary: statisticId,
|
||||
iconPath: mdiShape,
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<div class="container">
|
||||
${!this._opened
|
||||
? html`
|
||||
<ha-combo-box-item
|
||||
.disabled=${this.disabled}
|
||||
id="anchor"
|
||||
type="button"
|
||||
compact
|
||||
@click=${this._showPicker}
|
||||
>
|
||||
${this._renderContent()}
|
||||
</ha-combo-box-item>
|
||||
`
|
||||
: html`
|
||||
<ha-statistic-combo-box
|
||||
id="input"
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.allowCustomEntity=${this.allowCustomEntity}
|
||||
.label=${this.hass.localize("ui.common.search")}
|
||||
.value=${this.value}
|
||||
.includeStatisticsUnitOfMeasurement=${this
|
||||
.includeStatisticsUnitOfMeasurement}
|
||||
.includeUnitClass=${this.includeUnitClass}
|
||||
.includeDeviceClass=${this.includeDeviceClass}
|
||||
.statisticTypes=${this.statisticTypes}
|
||||
.statisticIds=${this.statisticIds}
|
||||
.excludeStatistics=${this.excludeStatistics}
|
||||
hide-clear-icon
|
||||
@opened-changed=${this._debounceOpenedChanged}
|
||||
@input=${stopPropagation}
|
||||
></ha-statistic-combo-box>
|
||||
`}
|
||||
${this._renderHelper()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private _clear(e) {
|
||||
e.stopPropagation();
|
||||
this.value = undefined;
|
||||
fireEvent(this, "value-changed", { value: undefined });
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
private async _showPicker() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
this._opened = true;
|
||||
await this.updateComplete;
|
||||
this._input?.focus();
|
||||
this._input?.open();
|
||||
}
|
||||
|
||||
// Multiple calls to _openedChanged can be triggered in quick succession
|
||||
// when the menu is opened
|
||||
private _debounceOpenedChanged = debounce(
|
||||
(ev) => this._openedChanged(ev),
|
||||
10
|
||||
);
|
||||
|
||||
private async _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
|
||||
const opened = ev.detail.value;
|
||||
if (this._opened && !opened) {
|
||||
this._opened = false;
|
||||
await this.updateComplete;
|
||||
this._anchor?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: string) {
|
||||
this.value = value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
.container {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
ha-combo-box-item {
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-radius: 4px;
|
||||
border-end-end-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
--md-list-item-one-line-container-height: 56px;
|
||||
--md-list-item-two-line-container-height: 56px;
|
||||
--md-list-item-top-space: 8px;
|
||||
--md-list-item-bottom-space: 8px;
|
||||
--md-list-item-leading-space: 8px;
|
||||
--md-list-item-trailing-space: 8px;
|
||||
--ha-md-list-item-gap: 8px;
|
||||
/* Remove the default focus ring */
|
||||
--md-focus-ring-width: 0px;
|
||||
--md-focus-ring-duration: 0s;
|
||||
}
|
||||
|
||||
/* Add Similar focus style as the text field */
|
||||
ha-combo-box-item:after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: var(
|
||||
--mdc-text-field-idle-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
transform:
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
}
|
||||
|
||||
ha-combo-box-item:focus:after {
|
||||
height: 2px;
|
||||
background-color: var(--mdc-theme-primary);
|
||||
}
|
||||
|
||||
ha-combo-box-item ha-svg-icon[slot="start"] {
|
||||
margin: 0 4px;
|
||||
}
|
||||
.clear {
|
||||
margin: 0 -8px;
|
||||
--mdc-icon-button-size: 32px;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
.edit {
|
||||
--mdc-icon-size: 20px;
|
||||
width: 32px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.placeholder {
|
||||
color: var(--secondary-text-color);
|
||||
padding: 0 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -16,11 +16,11 @@ class HaStatisticsPicker extends LitElement {
|
||||
@property({ attribute: "statistic-types" })
|
||||
public statisticTypes?: "mean" | "sum";
|
||||
|
||||
@property({ attribute: "picked-statistic-label" })
|
||||
public pickedStatisticLabel?: string;
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
@property({ attribute: "pick-statistic-label" })
|
||||
public pickStatisticLabel?: string;
|
||||
@property({ type: String })
|
||||
public placeholder?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||
public allowCustomEntity;
|
||||
@@ -82,6 +82,7 @@ class HaStatisticsPicker extends LitElement {
|
||||
: this.statisticTypes;
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
${repeat(
|
||||
this._currentStatistics,
|
||||
(statisticId) => statisticId,
|
||||
@@ -96,7 +97,6 @@ class HaStatisticsPicker extends LitElement {
|
||||
.value=${statisticId}
|
||||
.statisticTypes=${includeStatisticTypesCurrent}
|
||||
.statisticIds=${this.statisticIds}
|
||||
.label=${this.pickedStatisticLabel}
|
||||
.excludeStatistics=${this.value}
|
||||
.allowCustomEntity=${this.allowCustomEntity}
|
||||
@value-changed=${this._statisticChanged}
|
||||
@@ -113,7 +113,7 @@ class HaStatisticsPicker extends LitElement {
|
||||
.includeDeviceClass=${this.includeDeviceClass}
|
||||
.statisticTypes=${this.statisticTypes}
|
||||
.statisticIds=${this.statisticIds}
|
||||
.label=${this.pickStatisticLabel}
|
||||
.placeholder=${this.placeholder}
|
||||
.excludeStatistics=${this.value}
|
||||
.allowCustomEntity=${this.allowCustomEntity}
|
||||
@value-changed=${this._addStatistic}
|
||||
@@ -181,6 +181,10 @@ class HaStatisticsPicker extends LitElement {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0 0 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -108,7 +108,7 @@ class StateInfo extends LitElement {
|
||||
|
||||
.name.in-dialog,
|
||||
:host([secondary-line]) .name {
|
||||
line-height: var(--ha-line-height-normal);
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.time-ago,
|
||||
|
@@ -129,7 +129,7 @@ class HaAlert extends LitElement {
|
||||
}
|
||||
.title {
|
||||
margin-top: 2px;
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: bold;
|
||||
}
|
||||
.action mwc-button,
|
||||
.action ha-icon-button {
|
||||
|
@@ -56,7 +56,7 @@ export class HaAnsiToHtml extends LitElement {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.bold {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: bold;
|
||||
}
|
||||
.italic {
|
||||
font-style: italic;
|
||||
|
@@ -44,7 +44,7 @@ export class HaAreasDisplayEditor extends LitElement {
|
||||
);
|
||||
|
||||
const items: DisplayItem[] = areas.map((area) => {
|
||||
const { floor } = getAreaContext(area.area_id, this.hass!);
|
||||
const { floor } = getAreaContext(area, this.hass!);
|
||||
return {
|
||||
value: area.area_id,
|
||||
label: area.name,
|
||||
|
@@ -595,7 +595,7 @@ export class HaAssistChat extends LitElement {
|
||||
}
|
||||
.message {
|
||||
white-space: pre-line;
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-size: 18px;
|
||||
clear: both;
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
@@ -604,7 +604,7 @@ export class HaAssistChat extends LitElement {
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.message {
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -95,23 +95,23 @@ export class HaBadge extends LitElement {
|
||||
text-align: center;
|
||||
}
|
||||
.label {
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: var(--ha-font-weight-semibold);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
font-weight: 500;
|
||||
line-height: 10px;
|
||||
letter-spacing: 0.1px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.content {
|
||||
font-size: var(--ha-font-size-badge, var(--ha-font-size-s));
|
||||
font-size: var(--ha-badge-font-size, var(--ha-font-size-s));
|
||||
font-style: normal;
|
||||
font-weight: var(--ha-font-weight-semibold);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.1px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
::slotted([slot="icon"]) {
|
||||
--mdc-icon-size: var(--ha-icon-size-badge, 18px);
|
||||
--mdc-icon-size: var(--ha-badge-icon-size, 18px);
|
||||
color: var(--badge-color);
|
||||
line-height: 0;
|
||||
margin-left: -4px;
|
||||
|
@@ -387,15 +387,9 @@ export class HaBaseTimeInput extends LitElement {
|
||||
--mdc-typography-body2-font-family,
|
||||
var(--mdc-typography-font-family, Roboto, sans-serif)
|
||||
);
|
||||
font-size: var(--mdc-typography-body2-font-size, var(--ha-font-size-s));
|
||||
line-height: var(
|
||||
--mdc-typography-body2-line-height,
|
||||
var(--ha-line-height-normal)
|
||||
);
|
||||
font-weight: var(
|
||||
--mdc-typography-body2-font-weight,
|
||||
var(--ha-font-weight-normal)
|
||||
);
|
||||
font-size: var(--mdc-typography-body2-font-size, 0.875rem);
|
||||
line-height: var(--mdc-typography-body2-line-height, 1.25rem);
|
||||
font-weight: var(--mdc-typography-body2-font-weight, 400);
|
||||
letter-spacing: var(
|
||||
--mdc-typography-body2-letter-spacing,
|
||||
0.0178571429em
|
||||
@@ -409,7 +403,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
}
|
||||
ha-input-helper-text {
|
||||
padding-top: 8px;
|
||||
line-height: var(--ha-line-height-normal);
|
||||
line-height: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -59,7 +59,7 @@ export class HaBigNumber extends LitElement {
|
||||
css`
|
||||
:host {
|
||||
font-size: 57px;
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
line-height: 1.12;
|
||||
letter-spacing: -0.25px;
|
||||
}
|
||||
.value {
|
||||
@@ -87,12 +87,12 @@ export class HaBigNumber extends LitElement {
|
||||
}
|
||||
.value .decimal {
|
||||
font-size: 0.42em;
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
line-height: 1.33;
|
||||
min-height: 1.33em;
|
||||
}
|
||||
.value .unit {
|
||||
font-size: 0.33em;
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
line-height: 1.26;
|
||||
}
|
||||
/* Accessibility */
|
||||
.visually-hidden {
|
||||
|
@@ -41,14 +41,14 @@ export class HaCard extends LitElement {
|
||||
:host ::slotted(.card-header) {
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
|
||||
font-size: var(--ha-card-header-font-size, 24px);
|
||||
letter-spacing: -0.012em;
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
line-height: 48px;
|
||||
padding: 12px 16px 16px;
|
||||
display: block;
|
||||
margin-block-start: 0px;
|
||||
margin-block-end: 0px;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
:host ::slotted(.card-content:not(:first-child)),
|
||||
|
@@ -154,7 +154,7 @@ class HaClimateState extends LitElement {
|
||||
}
|
||||
|
||||
.state-label {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.unit {
|
||||
|
@@ -17,14 +17,17 @@ export class HaComboBoxItem extends HaMdListItem {
|
||||
:host([border-top]) md-item {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
[slot="start"] {
|
||||
--state-icon-color: var(--secondary-text-color);
|
||||
}
|
||||
[slot="headline"] {
|
||||
line-height: var(--ha-line-height-normal);
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: 22px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
[slot="supporting-text"] {
|
||||
line-height: var(--ha-line-height-normal);
|
||||
font-size: var(--ha-font-size-s);
|
||||
line-height: 18px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
::slotted(state-badge),
|
||||
|
@@ -147,6 +147,10 @@ export class HaComboBox extends LitElement {
|
||||
this._comboBox.value = value;
|
||||
}
|
||||
|
||||
public setTextFieldValue(value: string) {
|
||||
this._inputElement.value = value;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<!-- @ts-ignore Tag definition is not included in theme folder -->
|
||||
|
@@ -59,7 +59,7 @@ export class HaControlButton extends LitElement {
|
||||
box-sizing: border-box;
|
||||
line-height: inherit;
|
||||
font-family: Roboto;
|
||||
font-weight: var(--ha-font-weight-semibold);
|
||||
font-weight: 500;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
background: none;
|
||||
|
@@ -194,7 +194,7 @@ export class HaControlNumberButton extends LitElement {
|
||||
color: var(--primary-text-color);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
font-style: normal;
|
||||
font-weight: var(--ha-font-weight-semibold);
|
||||
font-weight: 500;
|
||||
transition: color 180ms ease-in-out;
|
||||
}
|
||||
:host([disabled]) {
|
||||
|
@@ -179,8 +179,8 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
--control-select-menu-padding: 6px 10px;
|
||||
--mdc-icon-size: 20px;
|
||||
--ha-ripple-color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
width: auto;
|
||||
color: var(--primary-text-color);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
@@ -208,7 +208,7 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
font-style: normal;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.25px;
|
||||
}
|
||||
.content {
|
||||
|
@@ -207,7 +207,7 @@ export class HaControlSelect extends LitElement {
|
||||
outline: none;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
font-style: normal;
|
||||
font-weight: var(--ha-font-weight-semibold);
|
||||
font-weight: 500;
|
||||
color: var(--primary-text-color);
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
@@ -368,7 +368,7 @@ export class HaControlSlider extends LitElement {
|
||||
--control-slider-background-opacity: 0.2;
|
||||
--control-slider-thickness: 40px;
|
||||
--control-slider-border-radius: 10px;
|
||||
--control-slider-tooltip-font-size: var(--ha-font-size-m);
|
||||
--control-slider-tooltip-font-size: 14px;
|
||||
height: var(--control-slider-thickness);
|
||||
width: 100%;
|
||||
border-radius: var(--control-slider-border-radius);
|
||||
|
@@ -53,13 +53,13 @@ export class HaDialogHeader extends LitElement {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.header-title {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.header-subtitle {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
@media all and (min-width: 450px) and (min-height: 500px) {
|
||||
|
@@ -85,8 +85,8 @@ export class HaDialog extends DialogBase {
|
||||
var(--dialog-backdrop-filter, none)
|
||||
);
|
||||
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
--mdc-typography-headline6-font-size: var(--ha-font-size-xl);
|
||||
--mdc-typography-headline6-font-weight: 400;
|
||||
--mdc-typography-headline6-font-size: 1.574rem;
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
justify-content: var(--justify-action-buttons, flex-end);
|
||||
|
@@ -188,7 +188,7 @@ export class HaExpansionPanel extends LitElement {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
font-weight: var(--ha-font-weight-semibold);
|
||||
font-weight: 500;
|
||||
outline: none;
|
||||
}
|
||||
#summary.noCollapse {
|
||||
@@ -218,7 +218,7 @@ export class HaExpansionPanel extends LitElement {
|
||||
.secondary {
|
||||
display: block;
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-size: 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -294,7 +294,7 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
.supports {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-size: 12px;
|
||||
}
|
||||
:host([disabled]) .secondary {
|
||||
color: var(--disabled-text-color);
|
||||
@@ -324,7 +324,7 @@ export class HaFileUpload extends LitElement {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.header {
|
||||
font-weight: var(--ha-font-weight-semibold);
|
||||
font-weight: 500;
|
||||
}
|
||||
.progress {
|
||||
color: var(--secondary-text-color);
|
||||
@@ -333,7 +333,7 @@ export class HaFileUpload extends LitElement {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-size: 14px;
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
|
@@ -208,10 +208,10 @@ export class HaFilterBlueprints extends LitElement {
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
|
@@ -303,10 +303,10 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
|
@@ -232,10 +232,10 @@ export class HaFilterDevices extends LitElement {
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
|
@@ -189,10 +189,10 @@ export class HaFilterDomains extends LitElement {
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
|
@@ -246,10 +246,10 @@ export class HaFilterEntities extends LitElement {
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
|
@@ -303,10 +303,10 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
|
@@ -195,10 +195,10 @@ export class HaFilterIntegrations extends LitElement {
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
|
@@ -233,10 +233,10 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
|
@@ -177,10 +177,10 @@ export class HaFilterStates extends LitElement {
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
|
@@ -70,11 +70,8 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
|
||||
padding-top: 4px;
|
||||
box-sizing: border-box;
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(
|
||||
--mdc-typography-body2-font-weight,
|
||||
var(--ha-font-weight-normal)
|
||||
);
|
||||
font-size: 0.875rem;
|
||||
font-weight: var(--mdc-typography-body2-font-weight, 400);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ export class HaFormConstant extends LitElement implements HaFormElement {
|
||||
display: block;
|
||||
}
|
||||
.label {
|
||||
font-weight: var(--ha-font-weight-semibold);
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -46,7 +46,7 @@ export class HaHeaderBar extends LitElement {
|
||||
flex: none;
|
||||
}
|
||||
.mdc-top-app-bar__title {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-size: 20px;
|
||||
padding-inline-start: 24px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
|
@@ -34,11 +34,8 @@ export class HaBadge extends LitElement {
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
color: var(--ha-heading-badge-text-color, var(--secondary-text-color));
|
||||
font-size: var(--ha-heading-badge-font-size, var(--ha-font-size-m));
|
||||
font-weight: var(
|
||||
--ha-heading-badge-font-weight,
|
||||
var(--ha-font-weight-normal)
|
||||
);
|
||||
font-size: var(--ha-heading-badge-font-size, 14px);
|
||||
font-weight: var(--ha-heading-badge-font-weight, 400);
|
||||
line-height: var(--ha-heading-badge-line-height, 20px);
|
||||
letter-spacing: 0.1px;
|
||||
--mdc-icon-size: 14px;
|
||||
|
@@ -103,7 +103,7 @@ class HaHumidifierState extends LitElement {
|
||||
}
|
||||
|
||||
.state-label {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.unit {
|
||||
|
@@ -12,7 +12,7 @@ class InputHelperText extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-size: 0.75rem;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding-inline-start: 16px;
|
||||
|
@@ -80,7 +80,7 @@ class HaLabelBadge extends LitElement {
|
||||
/* Make the label as wide as container+border. (parent_borderwidth / font-size) */
|
||||
left: -0.2em;
|
||||
right: -0.2em;
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
line-height: 1em;
|
||||
font-size: 0.5em;
|
||||
}
|
||||
.label-badge .label span {
|
||||
@@ -91,7 +91,7 @@ class HaLabelBadge extends LitElement {
|
||||
color: var(--ha-label-badge-label-color, white);
|
||||
border-radius: 1em;
|
||||
padding: 9% 16% 8% 16%; /* mostly apitalized text, not much descenders => bit more top margin */
|
||||
font-weight: var(--ha-font-weight-semibold);
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-transform: uppercase;
|
||||
text-overflow: ellipsis;
|
||||
@@ -102,10 +102,7 @@ class HaLabelBadge extends LitElement {
|
||||
margin-top: 1em;
|
||||
font-size: var(--ha-label-badge-title-font-size, 0.9em);
|
||||
width: var(--ha-label-badge-title-width, 5em);
|
||||
font-weight: var(
|
||||
--ha-label-badge-title-font-weight,
|
||||
var(--ha-font-weight-normal)
|
||||
);
|
||||
font-weight: var(--ha-label-badge-title-font-weight, 400);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: normal;
|
||||
|
@@ -32,9 +32,9 @@ class HaLabel extends LitElement {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-semibold);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.1px;
|
||||
vertical-align: middle;
|
||||
height: 32px;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user