diff --git a/.github/workflows/relative-ci.yaml b/.github/workflows/relative-ci.yaml
index 627215abb9..e82fe096c5 100644
--- a/.github/workflows/relative-ci.yaml
+++ b/.github/workflows/relative-ci.yaml
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
- uses: relative-ci/agent-action@v2.2.0
+ uses: relative-ci/agent-action@v3.0.0
with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}
diff --git a/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch b/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch
deleted file mode 100644
index 1f2665c2df..0000000000
--- a/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch
+++ /dev/null
@@ -1,18 +0,0 @@
-diff --git a/dist/hls.light.mjs b/dist/hls.light.mjs
-index eed9d788fafdb159975e1a2eb08ac88ba9c9ac33..ace881935e6665946f1c8110ebd2f739cde4427e 100644
---- a/dist/hls.light.mjs
-+++ b/dist/hls.light.mjs
-@@ -20523,9 +20523,9 @@ class Hls {
- }
- Hls.defaultConfig = void 0;
-
--var KeySystemFormats = empty.KeySystemFormats;
--var KeySystems = empty.KeySystems;
--var SubtitleStreamController = empty.SubtitleStreamController;
--var TimelineController = empty.TimelineController;
-+var KeySystemFormats = empty;
-+var KeySystems = empty;
-+var SubtitleStreamController = empty;
-+var TimelineController = empty;
- export { AbrController, AttrList, Cues as AudioStreamController, Cues as AudioTrackController, BasePlaylistController, BaseSegment, BaseStreamController, BufferController, Cues as CMCDController, CapLevelController, ChunkMetadata, ContentSteeringController, DateRange, Cues as EMEController, ErrorActionFlags, ErrorController, ErrorDetails, ErrorTypes, Events, FPSController, Fragment, Hls, HlsSkip, HlsUrlParameters, KeySystemFormats, KeySystems, Level, LevelDetails, LevelKey, LoadStats, MetadataSchema, NetworkErrorAction, Part, PlaylistLevelType, SubtitleStreamController, Cues as SubtitleTrackController, TimelineController, Hls as default, getMediaSource, isMSESupported, isSupported };
- //# sourceMappingURL=hls.light.mjs.map
diff --git a/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch b/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch
similarity index 100%
rename from .yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch
rename to .yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch
diff --git a/cast/src/launcher/layout/hc-connect.ts b/cast/src/launcher/layout/hc-connect.ts
index 17dee754c7..31f88ae8eb 100644
--- a/cast/src/launcher/layout/hc-connect.ts
+++ b/cast/src/launcher/layout/hc-connect.ts
@@ -302,7 +302,7 @@ export class HcConnect extends LitElement {
}
.error {
color: red;
- font-weight: bold;
+ font-weight: var(--ha-font-weight-bold);
}
.error a {
diff --git a/cast/src/launcher/layout/hc-layout.ts b/cast/src/launcher/layout/hc-layout.ts
index 87cb2cced1..c34a10f874 100644
--- a/cast/src/launcher/layout/hc-layout.ts
+++ b/cast/src/launcher/layout/hc-layout.ts
@@ -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, 24px);
+ font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
letter-spacing: -0.012em;
- line-height: 32px;
+ line-height: var(--ha-line-height-condensed);
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: 14px;
+ font-size: var(--ha-font-size-m);
color: var(--secondary-text-color);
line-height: initial;
}
@@ -113,7 +113,7 @@ class HcLayout extends LitElement {
}
:host ::slotted(.section-header) {
- font-weight: 500;
+ font-weight: var(--ha-font-weight-medium);
padding: 4px 16px;
text-transform: uppercase;
}
@@ -135,7 +135,7 @@ class HcLayout extends LitElement {
.footer {
text-align: center;
- font-size: 12px;
+ font-size: var(--ha-font-size-s);
padding: 8px 0 24px;
color: var(--secondary-text-color);
}
diff --git a/cast/src/receiver/layout/hc-launch-screen.ts b/cast/src/receiver/layout/hc-launch-screen.ts
index cab6840ba4..64b50839ee 100644
--- a/cast/src/receiver/layout/hc-launch-screen.ts
+++ b/cast/src/receiver/layout/hc-launch-screen.ts
@@ -29,7 +29,7 @@ class HcLaunchScreen extends LitElement {
display: block;
height: 100vh;
background-color: #f2f4f9;
- font-size: 24px;
+ font-size: var(--ha-font-size-2xl);
}
.container {
display: flex;
diff --git a/demo/src/html/index.html.template b/demo/src/html/index.html.template
index adcae133fb..09a6e7b514 100644
--- a/demo/src/html/index.html.template
+++ b/demo/src/html/index.html.template
@@ -68,7 +68,7 @@
}
#ha-launch-screen .ha-launch-screen-spacer-top {
flex: 1;
- margin-top: calc( 2 * max(env(safe-area-inset-bottom), 48px) + 46px );
+ margin-top: calc( 2 * max(var(--safe-area-inset-bottom), 48px) + 46px );
padding-top: 48px;
}
#ha-launch-screen .ha-launch-screen-spacer-bottom {
@@ -76,7 +76,7 @@
padding-top: 48px;
}
.ohf-logo {
- margin: max(env(safe-area-inset-bottom), 48px) 0;
+ margin: max(var(--safe-area-inset-bottom), 48px) 0;
display: flex;
flex-direction: column;
align-items: center;
diff --git a/demo/src/stubs/frontend.ts b/demo/src/stubs/frontend.ts
index ae4ac073fd..70a4d5a0d2 100644
--- a/demo/src/stubs/frontend.ts
+++ b/demo/src/stubs/frontend.ts
@@ -1,7 +1,30 @@
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
+let changeFunction;
+
export const mockFrontend = (hass: MockHomeAssistant) => {
hass.mockWS("frontend/get_user_data", () => ({
value: null,
}));
+ hass.mockWS("frontend/set_user_data", ({ key, value }) => {
+ if (key === "sidebar") {
+ changeFunction?.({
+ value: {
+ panelOrder: value.panelOrder || [],
+ hiddenPanels: value.hiddenPanels || [],
+ },
+ });
+ }
+ });
+ hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
+ changeFunction = onChange;
+ onChange?.({
+ value: {
+ panelOrder: [],
+ hiddenPanels: [],
+ },
+ });
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ return () => {};
+ });
};
diff --git a/gallery/src/components/page-description.ts b/gallery/src/components/page-description.ts
index 9c98d9b37e..10ad924be9 100644
--- a/gallery/src/components/page-description.ts
+++ b/gallery/src/components/page-description.ts
@@ -38,12 +38,12 @@ class PageDescription extends HaMarkdown {
}
.title {
font-size: 42px;
- line-height: 56px;
+ line-height: var(--ha-line-height-condensed);
padding-bottom: 8px;
}
.subtitle {
- font-size: 18px;
- line-height: 24px;
+ font-size: var(--ha-font-size-l);
+ line-height: var(--ha-line-height-normal);
}
.root {
max-width: 800px;
diff --git a/gallery/src/ha-demo-options.ts b/gallery/src/ha-demo-options.ts
index f3565e7891..4ec1927f7b 100644
--- a/gallery/src/ha-demo-options.ts
+++ b/gallery/src/ha-demo-options.ts
@@ -34,7 +34,7 @@ class HaDemoOptions extends LitElement {
height: 64px;
padding: 0 16px;
pointer-events: none;
- font-size: 20px;
+ font-size: var(--ha-font-size-xl);
}
`,
];
diff --git a/gallery/src/ha-gallery.ts b/gallery/src/ha-gallery.ts
index d1043815a2..b4d227488d 100644
--- a/gallery/src/ha-gallery.ts
+++ b/gallery/src/ha-gallery.ts
@@ -250,14 +250,14 @@ class HaGallery extends LitElement {
}
.page-footer .header {
- font-size: 16px;
- font-weight: 500;
- line-height: 28px;
+ font-size: var(--ha-font-size-l);
+ font-weight: var(--ha-font-weight-medium);
+ line-height: var(--ha-line-height-normal);
text-align: center;
}
.page-footer .secondary {
- line-height: 23px;
+ line-height: var(--ha-line-height-normal);
text-align: center;
}
diff --git a/gallery/src/pages/components/ha-control-button.ts b/gallery/src/pages/components/ha-control-button.ts
index 96f10d9625..5d2daf09fc 100644
--- a/gallery/src/pages/components/ha-control-button.ts
+++ b/gallery/src/pages/components/ha-control-button.ts
@@ -150,7 +150,7 @@ export class DemoHaBarButton extends LitElement {
margin: 0;
}
label {
- font-weight: 600;
+ font-weight: var(--ha-font-weight-bold);
}
.custom {
--control-button-icon-color: var(--primary-color);
diff --git a/gallery/src/pages/components/ha-control-number-buttons.ts b/gallery/src/pages/components/ha-control-number-buttons.ts
index 29496fc689..1b99117a50 100644
--- a/gallery/src/pages/components/ha-control-number-buttons.ts
+++ b/gallery/src/pages/components/ha-control-number-buttons.ts
@@ -86,7 +86,7 @@ export class DemoHarControlNumberButtons extends LitElement {
margin: 0;
}
label {
- font-weight: 600;
+ font-weight: var(--ha-font-weight-bold);
}
.custom {
color: #2196f3;
diff --git a/gallery/src/pages/components/ha-control-select-menu.ts b/gallery/src/pages/components/ha-control-select-menu.ts
index 638f682b0c..6050c2639b 100644
--- a/gallery/src/pages/components/ha-control-select-menu.ts
+++ b/gallery/src/pages/components/ha-control-select-menu.ts
@@ -125,7 +125,7 @@ export class DemoHaControlSelectMenu extends LitElement {
margin: 0;
}
label {
- font-weight: 600;
+ font-weight: var(--ha-font-weight-bold);
}
.custom {
--control-button-icon-color: var(--primary-color);
diff --git a/gallery/src/pages/components/ha-control-select.ts b/gallery/src/pages/components/ha-control-select.ts
index f3887d0144..8666f42a1f 100644
--- a/gallery/src/pages/components/ha-control-select.ts
+++ b/gallery/src/pages/components/ha-control-select.ts
@@ -181,7 +181,7 @@ export class DemoHaControlSelect extends LitElement {
margin: 0;
}
label {
- font-weight: 600;
+ font-weight: var(--ha-font-weight-bold);
}
.custom {
--mdc-icon-size: 24px;
diff --git a/gallery/src/pages/components/ha-control-slider.ts b/gallery/src/pages/components/ha-control-slider.ts
index b4af37cb67..5d8fb36cb5 100644
--- a/gallery/src/pages/components/ha-control-slider.ts
+++ b/gallery/src/pages/components/ha-control-slider.ts
@@ -144,7 +144,7 @@ export class DemoHaBarSlider extends LitElement {
margin: 0;
}
label {
- font-weight: 600;
+ font-weight: var(--ha-font-weight-bold);
}
.custom {
--control-slider-color: #ffcf4c;
diff --git a/gallery/src/pages/components/ha-control-switch.ts b/gallery/src/pages/components/ha-control-switch.ts
index 99ec957e9b..e175390948 100644
--- a/gallery/src/pages/components/ha-control-switch.ts
+++ b/gallery/src/pages/components/ha-control-switch.ts
@@ -112,7 +112,7 @@ export class DemoHaControlSwitch extends LitElement {
margin: 0;
}
label {
- font-weight: 600;
+ font-weight: var(--ha-font-weight-bold);
}
.custom {
--control-switch-on-color: var(--green-color);
diff --git a/gallery/src/pages/components/ha-hs-color-picker.ts b/gallery/src/pages/components/ha-hs-color-picker.ts
index c97ec7c8df..54c708c9df 100644
--- a/gallery/src/pages/components/ha-hs-color-picker.ts
+++ b/gallery/src/pages/components/ha-hs-color-picker.ts
@@ -105,8 +105,8 @@ export class DemoHaHsColorPicker extends LitElement {
width: 400px;
}
.value {
- font-size: 22px;
- font-weight: bold;
+ font-size: var(--ha-font-size-xl);
+ font-weight: var(--ha-font-weight-bold);
margin: 0 0 12px 0;
}
`;
diff --git a/gallery/src/pages/components/ha-select-box.ts b/gallery/src/pages/components/ha-select-box.ts
index ed2d182227..9cc5320424 100644
--- a/gallery/src/pages/components/ha-select-box.ts
+++ b/gallery/src/pages/components/ha-select-box.ts
@@ -123,7 +123,7 @@ export class DemoHaSelectBox extends LitElement {
margin: 0;
}
label {
- font-weight: 600;
+ font-weight: var(--ha-font-weight-bold);
margin-bottom: 8px;
display: block;
}
diff --git a/gallery/src/pages/components/ha-spinner.ts b/gallery/src/pages/components/ha-spinner.ts
index d84b13f399..3dc3eaae7b 100644
--- a/gallery/src/pages/components/ha-spinner.ts
+++ b/gallery/src/pages/components/ha-spinner.ts
@@ -1,6 +1,7 @@
import type { TemplateResult } from "lit";
-import { html, css, LitElement } from "lit";
+import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
+import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/ha-bar";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-spinner";
@@ -11,29 +12,66 @@ export class DemoHaSpinner extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
protected render(): TemplateResult {
- return html`
-
-
-
-
-
-
-
-
-
-
-
-
`;
+ return html`
+ ${["light", "dark"].map(
+ (mode) => html`
+
+ `
+ )}
+ `;
+ }
+
+ 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;
+ margin: 16px;
+ border-radius: 8px;
+ }
ha-card {
- max-width: 600px;
margin: 24px auto;
}
+ .card-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 24px;
+ }
`;
}
diff --git a/gallery/src/pages/date-time/date-time-numeric.ts b/gallery/src/pages/date-time/date-time-numeric.ts
index 0694e6edf0..e5659cdc6a 100644
--- a/gallery/src/pages/date-time/date-time-numeric.ts
+++ b/gallery/src/pages/date-time/date-time-numeric.ts
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeNumeric extends LitElement {
margin: 12px auto;
}
.header {
- font-weight: bold;
+ font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;
diff --git a/gallery/src/pages/date-time/date-time-seconds.ts b/gallery/src/pages/date-time/date-time-seconds.ts
index a2d88a0293..614696fa5b 100644
--- a/gallery/src/pages/date-time/date-time-seconds.ts
+++ b/gallery/src/pages/date-time/date-time-seconds.ts
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeSeconds extends LitElement {
margin: 12px auto;
}
.header {
- font-weight: bold;
+ font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;
diff --git a/gallery/src/pages/date-time/date-time-short-year.ts b/gallery/src/pages/date-time/date-time-short-year.ts
index 9e55a5c2c8..1bb04e54b1 100644
--- a/gallery/src/pages/date-time/date-time-short-year.ts
+++ b/gallery/src/pages/date-time/date-time-short-year.ts
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeShortYear extends LitElement {
margin: 12px auto;
}
.header {
- font-weight: bold;
+ font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;
diff --git a/gallery/src/pages/date-time/date-time-short.ts b/gallery/src/pages/date-time/date-time-short.ts
index 01a32fa32d..55206d928e 100644
--- a/gallery/src/pages/date-time/date-time-short.ts
+++ b/gallery/src/pages/date-time/date-time-short.ts
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeShort extends LitElement {
margin: 12px auto;
}
.header {
- font-weight: bold;
+ font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;
diff --git a/gallery/src/pages/date-time/date-time.ts b/gallery/src/pages/date-time/date-time.ts
index 6a61041d2b..30bac54815 100644
--- a/gallery/src/pages/date-time/date-time.ts
+++ b/gallery/src/pages/date-time/date-time.ts
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTime extends LitElement {
margin: 12px auto;
}
.header {
- font-weight: bold;
+ font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;
diff --git a/gallery/src/pages/date-time/date.ts b/gallery/src/pages/date-time/date.ts
index 12ee7244cc..3ae66b2325 100644
--- a/gallery/src/pages/date-time/date.ts
+++ b/gallery/src/pages/date-time/date.ts
@@ -92,7 +92,7 @@ export class DemoDateTimeDate extends LitElement {
static styles = css`
.header {
- font-weight: bold;
+ font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;
diff --git a/gallery/src/pages/date-time/time-seconds.ts b/gallery/src/pages/date-time/time-seconds.ts
index 6a7dc0a8a7..499ca100e7 100644
--- a/gallery/src/pages/date-time/time-seconds.ts
+++ b/gallery/src/pages/date-time/time-seconds.ts
@@ -106,7 +106,7 @@ export class DemoDateTimeTimeSeconds extends LitElement {
margin: 12px auto;
}
.header {
- font-weight: bold;
+ font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;
diff --git a/gallery/src/pages/date-time/time-weekday.ts b/gallery/src/pages/date-time/time-weekday.ts
index 68f24922ef..3f43b0e84d 100644
--- a/gallery/src/pages/date-time/time-weekday.ts
+++ b/gallery/src/pages/date-time/time-weekday.ts
@@ -106,7 +106,7 @@ export class DemoDateTimeTimeWeekday extends LitElement {
margin: 12px auto;
}
.header {
- font-weight: bold;
+ font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;
diff --git a/gallery/src/pages/date-time/time.ts b/gallery/src/pages/date-time/time.ts
index bc2c0135ce..e9a24b7cd6 100644
--- a/gallery/src/pages/date-time/time.ts
+++ b/gallery/src/pages/date-time/time.ts
@@ -106,7 +106,7 @@ export class DemoDateTimeTime extends LitElement {
margin: 12px auto;
}
.header {
- font-weight: bold;
+ font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;
diff --git a/hassio/src/addon-view/config/hassio-addon-config.ts b/hassio/src/addon-view/config/hassio-addon-config.ts
index e5afe3ed15..dcc845355e 100644
--- a/hassio/src/addon-view/config/hassio-addon-config.ts
+++ b/hassio/src/addon-view/config/hassio-addon-config.ts
@@ -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, 24px);
+ font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
letter-spacing: -0.012em;
- line-height: 48px;
+ line-height: var(--ha-line-height-expanded);
padding: 12px 16px 16px;
display: block;
margin-block: 0px;
- font-weight: normal;
+ font-weight: var(--ha-font-weight-normal);
}
.card-actions.right {
justify-content: flex-end;
diff --git a/hassio/src/addon-view/info/hassio-addon-info.ts b/hassio/src/addon-view/info/hassio-addon-info.ts
index 07a92c7b8e..e4c9c2cb17 100644
--- a/hassio/src/addon-view/info/hassio-addon-info.ts
+++ b/hassio/src/addon-view/info/hassio-addon-info.ts
@@ -1280,12 +1280,12 @@ class HassioAddonInfo extends LitElement {
padding-left: 8px;
padding-inline-start: 8px;
padding-inline-end: initial;
- font-size: 24px;
+ font-size: var(--ha-font-size-2xl);
color: var(--ha-card-header-color, var(--primary-text-color));
}
.addon-version {
float: var(--float-end);
- font-size: 15px;
+ font-size: var(--ha-font-size-l);
vertical-align: middle;
}
.errors {
diff --git a/hassio/src/backups/hassio-backups.ts b/hassio/src/backups/hassio-backups.ts
index 05cfe65ea4..acfb5a0ca6 100644
--- a/hassio/src/backups/hassio-backups.ts
+++ b/hassio/src/backups/hassio-backups.ts
@@ -391,7 +391,7 @@ export class HassioBackups extends LitElement {
top: -4px;
}
.selected-txt {
- font-weight: bold;
+ font-weight: var(--ha-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: 16px;
+ font-size: var(--ha-font-size-l);
}
.header-toolbar .header-btns {
margin-right: -12px;
diff --git a/hassio/src/components/hassio-card-content.ts b/hassio/src/components/hassio-card-content.ts
index 21027a1007..457df0437f 100644
--- a/hassio/src/components/hassio-card-content.ts
+++ b/hassio/src/components/hassio-card-content.ts
@@ -101,7 +101,7 @@ class HassioCardContent extends LitElement {
overflow: hidden;
position: relative;
height: 2.4em;
- line-height: 1.2em;
+ line-height: var(--ha-line-height-condensed);
}
.icon_image img {
max-height: 40px;
diff --git a/hassio/src/dashboard/hassio-dashboard.ts b/hassio/src/dashboard/hassio-dashboard.ts
index 531f171541..4cbf3c97b4 100644
--- a/hassio/src/dashboard/hassio-dashboard.ts
+++ b/hassio/src/dashboard/hassio-dashboard.ts
@@ -132,9 +132,9 @@ class HassioDashboard extends LitElement {
}
ha-fab.non-tabs {
position: fixed;
- right: calc(16px + env(safe-area-inset-right));
- bottom: calc(16px + env(safe-area-inset-bottom));
- inset-inline-end: calc(16px + env(safe-area-inset-right));
+ right: calc(16px + var(--safe-area-inset-right));
+ bottom: calc(16px + var(--safe-area-inset-bottom));
+ inset-inline-end: calc(16px + var(--safe-area-inset-right));
inset-inline-start: initial;
z-index: 1;
}
diff --git a/hassio/src/dashboard/hassio-update.ts b/hassio/src/dashboard/hassio-update.ts
index 369a80101d..5e5bd09396 100644
--- a/hassio/src/dashboard/hassio-update.ts
+++ b/hassio/src/dashboard/hassio-update.ts
@@ -131,7 +131,7 @@ export class HassioUpdate extends LitElement {
}
.update-heading {
font-size: var(--ha-font-size-l);
- font-weight: 500;
+ font-weight: var(--ha-font-weight-medium);
margin-bottom: 0.5em;
color: var(--primary-text-color);
}
diff --git a/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts b/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts
index 679014604f..dbf4aee778 100644
--- a/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts
+++ b/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts
@@ -173,7 +173,7 @@ class HassioHardwareDialog extends LitElement {
font-family: var(--ha-font-family-code);
}
code {
- font-size: 85%;
+ font-size: var(--ha-font-size-s);
padding: 0.2em 0.4em;
}
search-input {
diff --git a/hassio/src/dialogs/network/dialog-hassio-network.ts b/hassio/src/dialogs/network/dialog-hassio-network.ts
index f9b3a61613..862b81769a 100644
--- a/hassio/src/dialogs/network/dialog-hassio-network.ts
+++ b/hassio/src/dialogs/network/dialog-hassio-network.ts
@@ -610,7 +610,7 @@ export class DialogHassioNetwork
display: flex;
justify-content: space-between;
padding: 8px;
- padding-bottom: max(env(safe-area-inset-bottom), 8px);
+ padding-bottom: max(var(--safe-area-inset-bottom), 8px);
background-color: var(--mdc-theme-surface, #fff);
}
.warning {
diff --git a/hassio/src/entrypoint.ts b/hassio/src/entrypoint.ts
index 7efede19d2..73fa378323 100644
--- a/hassio/src/entrypoint.ts
+++ b/hassio/src/entrypoint.ts
@@ -1,3 +1,8 @@
+import {
+ haFontFamilyBody,
+ haFontSmoothing,
+ haMozOsxFontSmoothing,
+} from "../../src/resources/theme/typography.globals";
import "./hassio-main";
import("../../src/resources/append-ha-style");
@@ -5,10 +10,10 @@ import("../../src/resources/append-ha-style");
const styleEl = document.createElement("style");
styleEl.textContent = `
body {
- font-family: Roboto, sans-serif;
- -moz-osx-font-smoothing: grayscale;
- -webkit-font-smoothing: antialiased;
- font-weight: 400;
+ font-family: ${haFontFamilyBody};
+ -moz-osx-font-smoothing: ${haMozOsxFontSmoothing};
+ -webkit-font-smoothing: ${haFontSmoothing};
+ font-weight: var(--ha-font-weight-normal);
margin: 0;
padding: 0;
height: 100vh;
diff --git a/hassio/src/ingress-view/hassio-ingress-view.ts b/hassio/src/ingress-view/hassio-ingress-view.ts
index 1120936fa6..74545dc471 100644
--- a/hassio/src/ingress-view/hassio-ingress-view.ts
+++ b/hassio/src/ingress-view/hassio-ingress-view.ts
@@ -340,12 +340,12 @@ class HassioIngressView extends LitElement {
.header {
display: flex;
align-items: center;
- font-size: 16px;
+ font-size: var(--ha-font-size-l);
height: 40px;
padding: 0 16px;
pointer-events: none;
background-color: var(--app-header-background-color);
- font-weight: 400;
+ font-weight: var(--ha-font-weight-normal);
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: 20px;
+ line-height: var(--ha-line-height-condensed);
flex-grow: 1;
}
diff --git a/hassio/src/resources/hassio-style.ts b/hassio/src/resources/hassio-style.ts
index 1337e51d48..2979349546 100644
--- a/hassio/src/resources/hassio-style.ts
+++ b/hassio/src/resources/hassio-style.ts
@@ -14,6 +14,7 @@ export const hassioStyle = css`
margin-bottom: 8px;
font-family: var(--ha-font-family-body);
-webkit-font-smoothing: var(--ha-font-smoothing);
+ -moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
font-size: var(--ha-font-size-2xl);
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-condensed);
diff --git a/lint-staged.config.js b/lint-staged.config.js
index c09b35cc7d..d8e43a6fe7 100644
--- a/lint-staged.config.js
+++ b/lint-staged.config.js
@@ -4,7 +4,7 @@ export default {
"prettier --cache --write",
"lit-analyzer --quiet",
],
- "*.{json,css,md,markdown,html,y?aml}": "prettier --cache --write",
+ "*.{json,css,md,markdown,html,ya?ml}": "prettier --cache --write",
"translations/*/*.json": (files) =>
'printf "%s\n" "Translation files should not be added or modified here. Instead, make the necessary modifications in src/translations/en.json. Other languages are managed externally. Please see https://developers.home-assistant.io/docs/translations/ for details." ' +
files.join(" ") +
diff --git a/package.json b/package.json
index 8cabc96fab..5f9120e037 100644
--- a/package.json
+++ b/package.json
@@ -26,15 +26,15 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
- "@babel/runtime": "7.27.0",
+ "@babel/runtime": "7.27.1",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/commands": "6.8.1",
"@codemirror/language": "6.11.0",
"@codemirror/legacy-modes": "6.5.1",
- "@codemirror/search": "6.5.10",
+ "@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.2",
- "@codemirror/view": "6.36.6",
+ "@codemirror/view": "6.36.8",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.0",
"@formatjs/intl-displaynames": "6.8.11",
@@ -89,17 +89,17 @@
"@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.7",
+ "@vaadin/vaadin-themable-mixin": "24.7.7",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
- "barcode-detector": "3.0.1",
+ "barcode-detector": "3.0.4",
"color-name": "2.0.0",
"comlink": "4.4.2",
- "core-js": "3.41.0",
+ "core-js": "3.42.0",
"cropperjs": "1.6.2",
"date-fns": "4.1.0",
"date-fns-tz": "3.2.0",
@@ -111,9 +111,9 @@
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
- "hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
+ "hls.js": "1.6.2",
"home-assistant-js-websocket": "9.5.0",
- "idb-keyval": "6.2.1",
+ "idb-keyval": "6.2.2",
"intl-messageformat": "10.7.16",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
@@ -122,7 +122,7 @@
"lit": "3.3.0",
"lit-html": "3.3.0",
"luxon": "3.6.1",
- "marked": "15.0.11",
+ "marked": "15.0.12",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
@@ -131,13 +131,12 @@
"qrcode": "1.5.4",
"roboto-fontface": "0.10.0",
"rrule": "2.8.1",
- "sortablejs": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch",
+ "sortablejs": "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch",
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
"ua-parser-js": "2.0.3",
"vis-data": "7.1.9",
- "vis-network": "9.1.9",
"vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
@@ -150,18 +149,18 @@
"xss": "1.0.15"
},
"devDependencies": {
- "@babel/core": "7.26.10",
+ "@babel/core": "7.27.1",
"@babel/helper-define-polyfill-provider": "0.6.4",
- "@babel/plugin-transform-runtime": "7.26.10",
- "@babel/preset-env": "7.26.9",
- "@bundle-stats/plugin-webpack-filter": "4.19.1",
- "@lokalise/node-api": "14.4.0",
- "@octokit/auth-oauth-device": "7.1.5",
- "@octokit/plugin-retry": "7.2.1",
+ "@babel/plugin-transform-runtime": "7.27.1",
+ "@babel/preset-env": "7.27.2",
+ "@bundle-stats/plugin-webpack-filter": "4.20.1",
+ "@lokalise/node-api": "14.7.0",
+ "@octokit/auth-oauth-device": "8.0.1",
+ "@octokit/plugin-retry": "8.0.1",
"@octokit/rest": "21.1.1",
- "@rsdoctor/rspack-plugin": "1.0.2",
- "@rspack/cli": "1.3.7",
- "@rspack/core": "1.3.7",
+ "@rsdoctor/rspack-plugin": "1.1.2",
+ "@rspack/cli": "1.3.11",
+ "@rspack/core": "1.3.11",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.21",
"@types/chromecast-caf-sender": "1.0.11",
@@ -169,8 +168,8 @@
"@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
- "@types/leaflet": "1.9.17",
- "@types/leaflet-draw": "1.0.11",
+ "@types/leaflet": "1.9.18",
+ "@types/leaflet-draw": "1.0.12",
"@types/leaflet.markercluster": "1.5.5",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.6.2",
@@ -180,20 +179,20 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
- "@vitest/coverage-v8": "3.1.2",
+ "@vitest/coverage-v8": "3.1.4",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.0",
- "eslint": "9.25.1",
+ "eslint": "9.27.0",
"eslint-config-airbnb-base": "15.0.0",
- "eslint-config-prettier": "10.1.2",
+ "eslint-config-prettier": "10.1.5",
"eslint-import-resolver-webpack": "0.13.10",
"eslint-plugin-import": "2.31.0",
"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",
@@ -205,7 +204,7 @@
"husky": "9.1.7",
"jsdom": "26.1.0",
"jszip": "3.10.1",
- "lint-staged": "15.5.1",
+ "lint-staged": "15.5.2",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
@@ -219,9 +218,9 @@
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.8.3",
- "typescript-eslint": "8.31.0",
+ "typescript-eslint": "8.32.1",
"vite-tsconfig-paths": "5.1.4",
- "vitest": "3.1.2",
+ "vitest": "3.1.4",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
@@ -233,7 +232,7 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.0",
"@fullcalendar/daygrid": "6.1.17",
- "globals": "16.0.0",
+ "globals": "16.1.0",
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
},
diff --git a/pyproject.toml b/pyproject.toml
index 0a5a6fb847..762f3468b9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
-version = "20250516.0"
+version = "20250531.1"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"
diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts
index a8b254a552..dcd3ea4108 100644
--- a/src/auth/ha-authorize.ts
+++ b/src/auth/ha-authorize.ts
@@ -93,8 +93,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
background-color: var(--primary-background-color, #fafafa);
}
p {
- font-size: 14px;
- line-height: 20px;
+ font-size: var(--ha-font-size-m);
+ line-height: var(--ha-line-height-normal);
}
.card-content {
background: var(
@@ -151,8 +151,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
margin-inline-start: initial;
}
h1 {
- font-size: 28px;
- font-weight: 400;
+ font-size: var(--ha-font-size-3xl);
+ font-weight: var(--ha-font-weight-normal);
margin-top: 16px;
margin-bottom: 16px;
}
diff --git a/src/auth/ha-pick-auth-provider.ts b/src/auth/ha-pick-auth-provider.ts
index 9f6f11cd7b..2e48d21fb7 100644
--- a/src/auth/ha-pick-auth-provider.ts
+++ b/src/auth/ha-pick-auth-provider.ts
@@ -57,9 +57,9 @@ export class HaPickAuthProvider extends LitElement {
position: relative;
z-index: 1;
text-align: center;
- font-size: 14px;
- font-weight: 400;
- line-height: 20px;
+ font-size: var(--ha-font-size-m);
+ font-weight: var(--ha-font-weight-normal);
+ line-height: var(--ha-line-height-normal);
}
h3:before {
border-top: 1px solid var(--divider-color);
diff --git a/src/common/entity/state_icon.ts b/src/common/entity/state_icon.ts
index 58a63e881e..6eaef31725 100644
--- a/src/common/entity/state_icon.ts
+++ b/src/common/entity/state_icon.ts
@@ -2,7 +2,6 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "./compute_state_domain";
import { updateIcon } from "./update_icon";
import { deviceTrackerIcon } from "./device_tracker_icon";
-import { batteryIcon } from "./battery_icon";
export const stateIcon = (
stateObj: HassEntity,
@@ -10,17 +9,10 @@ export const stateIcon = (
): string | undefined => {
const domain = computeStateDomain(stateObj);
const compareState = state ?? stateObj.state;
- const dc = stateObj.attributes.device_class;
switch (domain) {
case "update":
return updateIcon(stateObj, compareState);
- case "sensor":
- if (dc === "battery") {
- return batteryIcon(stateObj, compareState);
- }
- break;
-
case "device_tracker":
return deviceTrackerIcon(stateObj, compareState);
diff --git a/src/common/entity/valid_service_id.ts b/src/common/entity/valid_service_id.ts
new file mode 100644
index 0000000000..97c88f6907
--- /dev/null
+++ b/src/common/entity/valid_service_id.ts
@@ -0,0 +1,4 @@
+const validServiceId = /^(\w+)\.(\w+)$/;
+
+export const isValidServiceId = (actionId: string) =>
+ validServiceId.test(actionId);
diff --git a/src/common/string/slugify.ts b/src/common/string/slugify.ts
index b7ddfed77c..fe5e0f537c 100644
--- a/src/common/string/slugify.ts
+++ b/src/common/string/slugify.ts
@@ -1,9 +1,19 @@
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
export const slugify = (value: string, delimiter = "_") => {
const a =
- "àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìıİłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·";
- const b = `aaaaaaaaaacccddeeeeeeeegghiiiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}`;
+ "àáâäæãåāăąабçćčđďдèéêëēėęěеёэфğǵгḧхîïíīįìıİийкłлḿмñńǹňнôöòóœøōõőоṕпŕřрßśšşșсťțтûüùúūǘůűųувẃẍÿýыžźżз·";
+ const b = `aaaaaaaaaaabcccdddeeeeeeeeeeefggghhiiiiiiiiijkllmmnnnnnoooooooooopprrrsssssstttuuuuuuuuuuvwxyyyzzzz${delimiter}`;
const p = new RegExp(a.split("").join("|"), "g");
+ const complex_cyrillic = {
+ ж: "zh",
+ х: "kh",
+ ц: "ts",
+ ч: "ch",
+ ш: "sh",
+ щ: "shch",
+ ю: "iu",
+ я: "ia",
+ };
let slugified;
@@ -14,6 +24,7 @@ export const slugify = (value: string, delimiter = "_") => {
.toString()
.toLowerCase()
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
+ .replace(/[а-я]/g, (c) => complex_cyrillic[c] || "") // Replace some cyrillic characters
.replace(/(\d),(?=\d)/g, "$1") // Remove Commas between numbers
.replace(/[^a-z0-9]+/g, delimiter) // Replace all non-word characters
.replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter
diff --git a/src/common/style/derived-css-vars.ts b/src/common/style/derived-css-vars.ts
index c4eca6e022..65e6f21d06 100644
--- a/src/common/style/derived-css-vars.ts
+++ b/src/common/style/derived-css-vars.ts
@@ -2,7 +2,7 @@ import type { CSSResult } from "lit";
const _extractCssVars = (
cssString: string,
- condition: (string) => boolean = () => true
+ condition: (string: string) => boolean = () => true
) => {
const variables: Record = {};
diff --git a/src/common/translations/markdown_support.ts b/src/common/translations/markdown_support.ts
new file mode 100644
index 0000000000..2cf7271a47
--- /dev/null
+++ b/src/common/translations/markdown_support.ts
@@ -0,0 +1,14 @@
+import { html } from "lit";
+import type { LocalizeFunc } from "./localize";
+
+const MARKDOWN_SUPPORT_URL = "https://commonmark.org/help/";
+
+export const supportsMarkdownHelper = (localize: LocalizeFunc) =>
+ localize("ui.common.supports_markdown", {
+ markdown_help_link: html`${localize("ui.common.markdown")}`,
+ });
diff --git a/src/components/chart/down-sample.ts b/src/components/chart/down-sample.ts
new file mode 100644
index 0000000000..9790de2a26
--- /dev/null
+++ b/src/components/chart/down-sample.ts
@@ -0,0 +1,72 @@
+import type { LineSeriesOption } from "echarts";
+
+export function downSampleLineData(
+ data: LineSeriesOption["data"],
+ chartWidth: number,
+ minX?: number,
+ maxX?: number
+) {
+ if (!data || data.length < 10) {
+ return data;
+ }
+ const width = chartWidth * window.devicePixelRatio;
+ if (data.length <= width) {
+ return data;
+ }
+ const min = minX ?? getPointData(data[0]!)[0];
+ const max = maxX ?? getPointData(data[data.length - 1]!)[0];
+ const step = Math.floor((max - min) / width);
+ const frames = new Map<
+ number,
+ {
+ min: { point: (typeof data)[number]; x: number; y: number };
+ max: { point: (typeof data)[number]; x: number; y: number };
+ }
+ >();
+
+ // Group points into frames
+ for (const point of data) {
+ const pointData = getPointData(point);
+ if (!Array.isArray(pointData)) continue;
+ const x = Number(pointData[0]);
+ const y = Number(pointData[1]);
+ if (isNaN(x) || isNaN(y)) continue;
+
+ const frameIndex = Math.floor((x - min) / step);
+ const frame = frames.get(frameIndex);
+ if (!frame) {
+ frames.set(frameIndex, { min: { point, x, y }, max: { point, x, y } });
+ } else {
+ if (frame.min.y > y) {
+ frame.min = { point, x, y };
+ }
+ if (frame.max.y < y) {
+ frame.max = { point, x, y };
+ }
+ }
+ }
+
+ // Convert frames back to points
+ const result: typeof data = [];
+ for (const [_i, frame] of frames) {
+ // Use min/max points to preserve visual accuracy
+ // The order of the data must be preserved so max may be before min
+ if (frame.min.x > frame.max.x) {
+ result.push(frame.max.point);
+ }
+ result.push(frame.min.point);
+ if (frame.min.x < frame.max.x) {
+ result.push(frame.max.point);
+ }
+ }
+
+ return result;
+}
+
+function getPointData(point: NonNullable[number]) {
+ const pointData =
+ point && typeof point === "object" && "value" in point
+ ? point.value
+ : point;
+ return pointData as number[];
+}
diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts
index fab103ebc2..37a12f07e6 100644
--- a/src/components/chart/ha-chart-base.ts
+++ b/src/components/chart/ha-chart-base.ts
@@ -27,6 +27,7 @@ import "../ha-icon-button";
import { formatTimeLabel } from "./axis-label";
import { ensureArray } from "../../common/array/ensure-array";
import "../chips/ha-assist-chip";
+import { downSampleLineData } from "./down-sample";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
const LEGEND_OVERFLOW_LIMIT = 10;
@@ -48,7 +49,8 @@ export class HaChartBase extends LitElement {
@property({ attribute: "expand-legend", type: Boolean })
public expandLegend?: boolean;
- @property({ attribute: false }) public extraComponents?: any[];
+ // extraComponents is not reactive and should not trigger updates
+ public extraComponents?: any[];
@state()
@consume({ context: themesContext, subscribe: true })
@@ -106,48 +108,49 @@ export class HaChartBase extends LitElement {
})
);
- // Add keyboard event listeners
- const handleKeyDown = (ev: KeyboardEvent) => {
- if (
- !this._modifierPressed &&
- ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
- ) {
- this._modifierPressed = true;
- if (!this.options?.dataZoom) {
- this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
+ if (!this.options?.dataZoom) {
+ // Add keyboard event listeners
+ const handleKeyDown = (ev: KeyboardEvent) => {
+ if (
+ !this._modifierPressed &&
+ ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
+ ) {
+ this._modifierPressed = true;
+ if (!this.options?.dataZoom) {
+ this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
+ }
+ // drag to zoom
+ this.chart?.dispatchAction({
+ type: "takeGlobalCursor",
+ key: "dataZoomSelect",
+ dataZoomSelectActive: true,
+ });
}
- // drag to zoom
- this.chart?.dispatchAction({
- type: "takeGlobalCursor",
- key: "dataZoomSelect",
- dataZoomSelectActive: true,
- });
- }
- };
+ };
- const handleKeyUp = (ev: KeyboardEvent) => {
- if (
- this._modifierPressed &&
- ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
- ) {
- this._modifierPressed = false;
- if (!this.options?.dataZoom) {
- this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
+ const handleKeyUp = (ev: KeyboardEvent) => {
+ if (
+ this._modifierPressed &&
+ ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
+ ) {
+ this._modifierPressed = false;
+ if (!this.options?.dataZoom) {
+ this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
+ }
+ this.chart?.dispatchAction({
+ type: "takeGlobalCursor",
+ key: "dataZoomSelect",
+ dataZoomSelectActive: false,
+ });
}
- this.chart?.dispatchAction({
- type: "takeGlobalCursor",
- key: "dataZoomSelect",
- dataZoomSelectActive: false,
- });
- }
- };
-
- window.addEventListener("keydown", handleKeyDown);
- window.addEventListener("keyup", handleKeyUp);
- this._listeners.push(
- () => window.removeEventListener("keydown", handleKeyDown),
- () => window.removeEventListener("keyup", handleKeyUp)
- );
+ };
+ window.addEventListener("keydown", handleKeyDown);
+ window.addEventListener("keyup", handleKeyUp);
+ this._listeners.push(
+ () => window.removeEventListener("keydown", handleKeyDown),
+ () => window.removeEventListener("keyup", handleKeyUp)
+ );
+ }
}
protected firstUpdated() {
@@ -191,16 +194,19 @@ export class HaChartBase extends LitElement {
${this._renderLegend()}
- ${this._isZoomed
- ? html``
- : nothing}
+
+ ${this._isZoomed
+ ? html``
+ : nothing}
+
+
`;
}
@@ -210,15 +216,15 @@ export class HaChartBase extends LitElement {
return nothing;
}
const legend = ensureArray(this.options.legend)[0] as LegendComponentOption;
- if (!legend.show) {
+ if (!legend.show || legend.type !== "custom") {
return nothing;
}
const datasets = ensureArray(this.data);
- const items = (legend.data ||
- datasets
+ const items: LegendComponentOption["data"] =
+ legend.data ||
+ ((datasets
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
- .map((d) => d.name ?? d.id) ||
- []) as string[];
+ .map((d) => d.name ?? d.id) || []) as string[]);
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
@@ -233,20 +239,32 @@ export class HaChartBase extends LitElement {
})}
>
- ${items.map((item: string, index: number) => {
+ ${items.map((item, index) => {
if (!this.expandLegend && index >= overflowLimit) {
return nothing;
}
- const dataset = datasets.find(
- (d) => d.id === item || d.name === item
- );
- const color = dataset?.color as string;
- const borderColor = dataset?.itemStyle?.borderColor as string;
+ let itemStyle: Record = {};
+ let name = "";
+ if (typeof item === "string") {
+ name = item;
+ const dataset = datasets.find(
+ (d) => d.id === item || d.name === item
+ );
+ itemStyle = {
+ color: dataset?.color as string,
+ ...(dataset?.itemStyle as { borderColor?: string }),
+ };
+ } else {
+ name = item.name ?? "";
+ itemStyle = item.itemStyle ?? {};
+ }
+ const color = itemStyle?.color as string;
+ const borderColor = itemStyle?.borderColor as string;
return html`-
-
${item}
+ ${name}
`;
})}
${items.length > overflowLimit
@@ -315,7 +333,9 @@ export class HaChartBase extends LitElement {
this.chart.on("click", (e: ECElementEvent) => {
fireEvent(this, "chart-click", e);
});
- this.chart.getZr().on("dblclick", this._handleClickZoom);
+ if (!this.options?.dataZoom) {
+ this.chart.getZr().on("dblclick", this._handleClickZoom);
+ }
if (this._isTouchDevice) {
this.chart.getZr().on("click", (e: ECElementEvent) => {
if (!e.zrByTouch) {
@@ -380,9 +400,9 @@ export class HaChartBase extends LitElement {
if (axis.type !== "time" || axis.show === false) {
return axis;
}
- if (axis.max && axis.min) {
+ if (axis.min) {
this._minutesDifference = differenceInMinutes(
- axis.max as Date,
+ (axis.max as Date) || new Date(),
axis.min as Date
);
}
@@ -410,6 +430,12 @@ export class HaChartBase extends LitElement {
} as XAXisOption;
});
}
+ let legend = this.options?.legend;
+ if (legend) {
+ legend = ensureArray(legend).map((l) =>
+ l.type === "custom" ? { show: false } : l
+ );
+ }
const options = {
animation: !this._reducedMotion,
darkMode: this._themes.darkMode ?? false,
@@ -424,7 +450,7 @@ export class HaChartBase extends LitElement {
iconStyle: { opacity: 0 },
},
...this.options,
- legend: { show: false },
+ legend,
xAxis,
};
@@ -468,6 +494,13 @@ export class HaChartBase extends LitElement {
smooth: false,
},
bar: { itemStyle: { barBorderWidth: 1.5 } },
+ graph: {
+ label: {
+ color: style.getPropertyValue("--primary-text-color"),
+ textBorderColor: style.getPropertyValue("--primary-background-color"),
+ textBorderWidth: 2,
+ },
+ },
categoryAxis: {
axisLine: { show: false },
axisTick: { show: false },
@@ -600,19 +633,21 @@ export class HaChartBase extends LitElement {
}
private _getSeries() {
- const series = ensureArray(this.data).filter(
- (d) => !this._hiddenDatasets.has(String(d.name ?? d.id))
- );
+ const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
+ | XAXisOption
+ | undefined;
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
| YAXisOption
| undefined;
- if (yAxis?.type === "log") {
- // set <=0 values to null so they render as gaps on a log graph
- return series.map((d) =>
- d.type === "line"
- ? {
- ...d,
- data: d.data?.map((v) =>
+ const series = ensureArray(this.data)
+ .filter((d) => !this._hiddenDatasets.has(String(d.name ?? d.id)))
+ .map((s) => {
+ if (s.type === "line") {
+ if (yAxis?.type === "log") {
+ // set <=0 values to null so they render as gaps on a log graph
+ return {
+ ...s,
+ data: s.data?.map((v) =>
Array.isArray(v)
? [
v[0],
@@ -621,10 +656,26 @@ export class HaChartBase extends LitElement {
]
: v
),
- }
- : d
- );
- }
+ };
+ }
+ if (s.sampling === "minmax") {
+ const minX =
+ xAxis?.min && typeof xAxis.min === "number"
+ ? xAxis.min
+ : undefined;
+ const maxX =
+ xAxis?.max && typeof xAxis.max === "number"
+ ? xAxis.max
+ : undefined;
+ return {
+ ...s,
+ sampling: undefined,
+ data: downSampleLineData(s.data, this.clientWidth, minX, maxX),
+ };
+ }
+ }
+ return s;
+ });
return series;
}
@@ -725,21 +776,31 @@ export class HaChartBase extends LitElement {
height: 100%;
width: 100%;
}
- .zoom-reset {
+ .chart-controls {
position: absolute;
top: 16px;
right: 4px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+ .chart-controls ha-icon-button,
+ .chart-controls ::slotted(ha-icon-button) {
background: var(--card-background-color);
border-radius: 4px;
--mdc-icon-button-size: 32px;
color: var(--primary-color);
border: 1px solid var(--divider-color);
}
+ .chart-controls ha-icon-button.inactive,
+ .chart-controls ::slotted(ha-icon-button.inactive) {
+ color: var(--state-inactive-color);
+ }
.chart-legend {
max-height: 60%;
overflow-y: auto;
padding: 12px 0 0;
- font-size: 12px;
+ font-size: var(--ha-font-size-s);
color: var(--primary-text-color);
}
.chart-legend ul {
diff --git a/src/components/chart/ha-network-graph.ts b/src/components/chart/ha-network-graph.ts
new file mode 100644
index 0000000000..c9693419dc
--- /dev/null
+++ b/src/components/chart/ha-network-graph.ts
@@ -0,0 +1,299 @@
+import type { EChartsType } from "echarts/core";
+import type { GraphSeriesOption } from "echarts/charts";
+import { css, html, LitElement, nothing } from "lit";
+import { customElement, property, state, query } from "lit/decorators";
+import type { TopLevelFormatterParams } from "echarts/types/dist/shared";
+import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
+import memoizeOne from "memoize-one";
+import { listenMediaQuery } from "../../common/dom/media_query";
+import type { ECOption } from "../../resources/echarts";
+import "./ha-chart-base";
+import type { HaChartBase } from "./ha-chart-base";
+import type { HomeAssistant } from "../../types";
+
+export interface NetworkNode {
+ id: string;
+ name?: string;
+ category?: number;
+ label?: string;
+ value?: number;
+ symbolSize?: number;
+ symbol?: string;
+ itemStyle?: {
+ color?: string;
+ borderColor?: string;
+ borderWidth?: number;
+ };
+ fixed?: boolean;
+ /**
+ * Distance from the center, where 0 is the center and 1 is the edge
+ */
+ polarDistance?: number;
+}
+
+export interface NetworkLink {
+ source: string;
+ target: string;
+ value?: number;
+ reverseValue?: number;
+ lineStyle?: {
+ width?: number;
+ color?: string;
+ type?: "solid" | "dashed" | "dotted";
+ };
+ symbolSize?: number | number[];
+ symbol?: string;
+ label?: {
+ show?: boolean;
+ formatter?: string;
+ };
+ ignoreForceLayout?: boolean;
+}
+
+export interface NetworkData {
+ nodes: NetworkNode[];
+ links: NetworkLink[];
+ categories?: { name: string; symbol: string }[];
+}
+
+// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
+let GraphChart: typeof import("echarts/lib/chart/graph/install");
+
+@customElement("ha-network-graph")
+export class HaNetworkGraph extends LitElement {
+ public chart?: EChartsType;
+
+ @property({ attribute: false }) public data!: NetworkData;
+
+ @property({ attribute: false }) public tooltipFormatter?: (
+ params: TopLevelFormatterParams
+ ) => string;
+
+ public hass!: HomeAssistant;
+
+ @state() private _reducedMotion = false;
+
+ @state() private _physicsEnabled = true;
+
+ @state() private _showLabels = true;
+
+ private _listeners: (() => void)[] = [];
+
+ private _nodePositions: Record = {};
+
+ @query("ha-chart-base") private _baseChart?: HaChartBase;
+
+ constructor() {
+ super();
+ if (!GraphChart) {
+ import("echarts/lib/chart/graph/install").then((module) => {
+ GraphChart = module;
+ this.requestUpdate();
+ });
+ }
+ }
+
+ public async connectedCallback() {
+ super.connectedCallback();
+ this._listeners.push(
+ listenMediaQuery("(prefers-reduced-motion)", (matches) => {
+ if (this._reducedMotion !== matches) {
+ this._reducedMotion = matches;
+ }
+ })
+ );
+ }
+
+ public disconnectedCallback() {
+ super.disconnectedCallback();
+ while (this._listeners.length) {
+ this._listeners.pop()!();
+ }
+ }
+
+ protected render() {
+ if (!GraphChart) {
+ return nothing;
+ }
+ return html`
+
+
+
+ `;
+ }
+
+ private _createOptions = memoizeOne(
+ (categories?: NetworkData["categories"]): ECOption => ({
+ tooltip: {
+ trigger: "item",
+ confine: true,
+ formatter: this.tooltipFormatter,
+ },
+ legend: {
+ show: !!categories?.length,
+ data: categories?.map((category) => ({
+ ...category,
+ icon: category.symbol,
+ })),
+ top: 8,
+ },
+ dataZoom: {
+ type: "inside",
+ filterMode: "none",
+ },
+ })
+ );
+
+ private _getSeries = memoizeOne(
+ (
+ data: NetworkData,
+ physicsEnabled: boolean,
+ reducedMotion: boolean,
+ showLabels: boolean
+ ) => {
+ const containerWidth = this.clientWidth;
+ const containerHeight = this.clientHeight;
+ return [
+ {
+ id: "network",
+ type: "graph",
+ layout: physicsEnabled ? "force" : "none",
+ draggable: true,
+ roam: true,
+ selectedMode: "single",
+ label: {
+ show: showLabels,
+ position: "right",
+ },
+ emphasis: {
+ focus: "adjacency",
+ },
+ force: {
+ repulsion: [400, 600],
+ edgeLength: [200, 300],
+ gravity: 0.1,
+ layoutAnimation: !reducedMotion && data.nodes.length < 100,
+ },
+ edgeSymbol: ["none", "arrow"],
+ edgeSymbolSize: 10,
+ data: data.nodes.map((node) => {
+ const echartsNode: NonNullable[number] =
+ {
+ id: node.id,
+ name: node.name,
+ category: node.category,
+ value: node.value,
+ symbolSize: node.symbolSize || 30,
+ symbol: node.symbol || "circle",
+ itemStyle: node.itemStyle || {},
+ fixed: node.fixed,
+ };
+ if (this._nodePositions[node.id]) {
+ echartsNode.x = this._nodePositions[node.id].x;
+ echartsNode.y = this._nodePositions[node.id].y;
+ } else if (typeof node.polarDistance === "number") {
+ // set the position of the node at polarDistance from the center in a random direction
+ const angle = Math.random() * 2 * Math.PI;
+ echartsNode.x =
+ containerWidth / 2 +
+ ((Math.cos(angle) * containerWidth) / 2) * node.polarDistance;
+ echartsNode.y =
+ containerHeight / 2 +
+ ((Math.sin(angle) * containerHeight) / 2) * node.polarDistance;
+ this._nodePositions[node.id] = {
+ x: echartsNode.x,
+ y: echartsNode.y,
+ };
+ }
+ return echartsNode;
+ }),
+ links: data.links.map((link) => ({
+ ...link,
+ value: link.reverseValue
+ ? Math.max(link.value ?? 0, link.reverseValue)
+ : link.value,
+ // remove arrow for bidirectional links
+ symbolSize: link.reverseValue ? 1 : link.symbolSize, // 0 doesn't work
+ })),
+ categories: data.categories || [],
+ },
+ ] as any;
+ }
+ );
+
+ private _togglePhysics() {
+ if (this._baseChart?.chart) {
+ this._baseChart.chart
+ // @ts-ignore private method but no other way to get the graph positions
+ .getModel()
+ .getSeriesByIndex(0)
+ .getGraph()
+ .eachNode((node: any) => {
+ const layout = node.getLayout();
+ if (layout) {
+ this._nodePositions[node.id] = {
+ x: layout[0],
+ y: layout[1],
+ };
+ }
+ });
+ }
+ this._physicsEnabled = !this._physicsEnabled;
+ }
+
+ private _toggleLabels() {
+ this._showLabels = !this._showLabels;
+ }
+
+ static styles = css`
+ :host {
+ display: block;
+ position: relative;
+ }
+ ha-chart-base {
+ height: 100%;
+ --chart-max-height: 100%;
+ }
+
+ ha-icon-button,
+ ::slotted(ha-icon-button) {
+ margin-right: 12px;
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-network-graph": HaNetworkGraph;
+ }
+ interface HASSDomEvents {
+ "node-selected": { id: string };
+ }
+}
diff --git a/src/components/chart/ha-sankey-chart.ts b/src/components/chart/ha-sankey-chart.ts
index dded2b6b99..67ba9a4f12 100644
--- a/src/components/chart/ha-sankey-chart.ts
+++ b/src/components/chart/ha-sankey-chart.ts
@@ -105,10 +105,41 @@ 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 indexes = [...new Set(filteredNodes.map((n) => n.index))].sort();
const depthMap = new Map();
- indexes.sort().forEach((index, i) => {
+ const sections: Node[][] = [];
+ indexes.forEach((index, i) => {
depthMap.set(index, i);
+ const nodesWithIndex = filteredNodes.filter((n) => n.index === index);
+ if (nodesWithIndex.length > 0) {
+ sections.push(
+ sections.length > 0
+ ? nodesWithIndex.sort((a, b) => {
+ // sort by the order of their parents in the previous section with orphans at the end
+ const aParentIndex = this._findParentIndex(
+ a.id,
+ data.links,
+ sections
+ );
+ const bParentIndex = this._findParentIndex(
+ b.id,
+ data.links,
+ sections
+ );
+ if (aParentIndex === bParentIndex) {
+ return 0;
+ }
+ if (aParentIndex === -1) {
+ return 1;
+ }
+ if (bParentIndex === -1) {
+ return -1;
+ }
+ return aParentIndex - bParentIndex;
+ })
+ : nodesWithIndex
+ );
+ }
});
const links = this._processLinks(filteredNodes, data.links);
const sectionWidth = width / indexes.length;
@@ -117,7 +148,7 @@ export class HaSankeyChart extends LitElement {
return {
id: "sankey",
type: "sankey",
- nodes: filteredNodes.map((node) => ({
+ nodes: sections.flat().map((node) => ({
id: node.id,
value: node.value,
itemStyle: {
@@ -227,6 +258,23 @@ export class HaSankeyChart extends LitElement {
return links;
}
+ private _findParentIndex(id: string, links: Link[], sections: Node[][]) {
+ const parent = links.find((l) => l.target === id)?.source;
+ if (!parent) {
+ return -1;
+ }
+ let offset = 0;
+ for (let i = sections.length - 1; i >= 0; i--) {
+ const section = sections[i];
+ const index = section.findIndex((n) => n.id === parent);
+ if (index !== -1) {
+ return offset + index;
+ }
+ offset += section.length;
+ }
+ return -1;
+ }
+
static styles = css`
:host {
display: block;
diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts
index 2ad664e627..f8f7689539 100644
--- a/src/components/chart/state-history-chart-line.ts
+++ b/src/components/chart/state-history-chart-line.ts
@@ -82,6 +82,8 @@ export class StateHistoryChartLine extends LitElement {
private _chartTime: Date = new Date();
+ private _previousYAxisLabelValue = 0;
+
protected render() {
return html`
Math.min(min, this.minYAxis!);
}
} else if (this.logarithmicScale) {
- minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
+ minYAxis = ({ min }) => {
+ const value = min > 0 ? min * 0.95 : min * 1.05;
+ return Math.abs(value) < 1 ? value : Math.floor(value);
+ };
}
if (typeof maxYAxis === "number") {
if (this.fitYData) {
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
}
} else if (this.logarithmicScale) {
- maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
+ maxYAxis = ({ max }) => {
+ const value = max > 0 ? max * 1.05 : max * 0.95;
+ return Math.abs(value) < 1 ? value : Math.ceil(value);
+ };
}
this._chartOptions = {
xAxis: {
@@ -258,35 +266,11 @@ export class StateHistoryChartLine extends LitElement {
},
axisLabel: {
margin: 5,
- formatter: (value: number) => {
- const formatOptions =
- value >= 1 || value <= -1
- ? undefined
- : {
- // show the first significant digit for tiny values
- maximumFractionDigits: Math.max(
- 2,
- -Math.floor(Math.log10(Math.abs(value % 1 || 1)))
- ),
- };
- const label = formatNumber(
- value,
- this.hass.locale,
- formatOptions
- );
- const width = measureTextWidth(label, 12) + 5;
- if (width > this._yWidth) {
- this._yWidth = width;
- fireEvent(this, "y-width-changed", {
- value: this._yWidth,
- chartIndex: this.chartIndex,
- });
- }
- return label;
- },
+ formatter: this._formatYAxisLabel,
},
} as YAXisOption,
legend: {
+ type: "custom",
show: this.showNames,
},
grid: {
@@ -744,14 +728,41 @@ export class StateHistoryChartLine extends LitElement {
this._visualMap = visualMap.length > 0 ? visualMap : undefined;
}
+ private _formatYAxisLabel = (value: number) => {
+ const formatOptions =
+ value >= 1 || value <= -1
+ ? undefined
+ : {
+ // show the first significant digit for tiny values
+ maximumFractionDigits: Math.max(
+ 2,
+ // use the difference to the previous value to determine the number of significant digits #25526
+ -Math.floor(
+ Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1))
+ )
+ ),
+ };
+ const label = formatNumber(value, this.hass.locale, formatOptions);
+ const width = measureTextWidth(label, 12) + 5;
+ if (width > this._yWidth) {
+ this._yWidth = width;
+ fireEvent(this, "y-width-changed", {
+ value: this._yWidth,
+ chartIndex: this.chartIndex,
+ });
+ }
+ this._previousYAxisLabelValue = value;
+ return label;
+ };
+
private _clampYAxis(value?: number | ((values: any) => number)) {
if (this.logarithmicScale) {
// log(0) is -Infinity, so we need to set a minimum value
if (typeof value === "number") {
- return Math.max(value, 0.1);
+ return Math.max(value, Number.EPSILON);
}
if (typeof value === "function") {
- return (values: any) => Math.max(value(values), 0.1);
+ return (values: any) => Math.max(value(values), Number.EPSILON);
}
}
return value;
diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts
index 57b66dac25..a1b7e30dc9 100644
--- a/src/components/chart/statistics-chart.ts
+++ b/src/components/chart/statistics-chart.ts
@@ -241,14 +241,20 @@ export class StatisticsChart extends LitElement {
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
}
} else if (this.logarithmicScale) {
- minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
+ minYAxis = ({ min }) => {
+ const value = min > 0 ? min * 0.95 : min * 1.05;
+ return Math.abs(value) < 1 ? value : Math.floor(value);
+ };
}
if (typeof maxYAxis === "number") {
if (this.fitYData) {
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
}
} else if (this.logarithmicScale) {
- maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
+ maxYAxis = ({ max }) => {
+ const value = max > 0 ? max * 1.05 : max * 0.95;
+ return Math.abs(value) < 1 ? value : Math.ceil(value);
+ };
}
const endTime = this.endTime ?? new Date();
let startTime = this.startTime;
@@ -308,6 +314,7 @@ export class StatisticsChart extends LitElement {
},
},
legend: {
+ type: "custom",
show: !this.hideLegend,
data: this._legendData,
},
@@ -618,10 +625,10 @@ export class StatisticsChart extends LitElement {
if (this.logarithmicScale) {
// log(0) is -Infinity, so we need to set a minimum value
if (typeof value === "number") {
- return Math.max(value, 0.1);
+ return Math.max(value, Number.EPSILON);
}
if (typeof value === "function") {
- return (values: any) => Math.max(value(values), 0.1);
+ return (values: any) => Math.max(value(values), Number.EPSILON);
}
}
return value;
diff --git a/src/components/chips/ha-assist-chip.ts b/src/components/chips/ha-assist-chip.ts
index 2775264aa4..275d2a7d20 100644
--- a/src/components/chips/ha-assist-chip.ts
+++ b/src/components/chips/ha-assist-chip.ts
@@ -60,7 +60,7 @@ export class HaAssistChip extends AssistChip {
opacity: var(--ha-assist-chip-active-container-opacity);
}
.label {
- font-family: Roboto, sans-serif;
+ font-family: var(--ha-font-family-body);
}
`,
];
diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts
index 66d7a3ba60..b0cf4bab88 100644
--- a/src/components/data-table/ha-data-table.ts
+++ b/src/components/data-table/ha-data-table.ts
@@ -164,6 +164,8 @@ export class HaDataTable extends LitElement {
@state() private _collapsedGroups: string[] = [];
+ @state() private _lastSelectedRowId: string | null = null;
+
private _checkableRowsCount?: number;
private _checkedRows: string[] = [];
@@ -187,6 +189,7 @@ export class HaDataTable extends LitElement {
public clearSelection(): void {
this._checkedRows = [];
+ this._lastSelectedRowId = null;
this._checkedRowsChanged();
}
@@ -194,6 +197,7 @@ export class HaDataTable extends LitElement {
this._checkedRows = this._filteredData
.filter((data) => data.selectable !== false)
.map((data) => data[this.id]);
+ this._lastSelectedRowId = null;
this._checkedRowsChanged();
}
@@ -207,6 +211,7 @@ export class HaDataTable extends LitElement {
this._checkedRows.push(id);
}
});
+ this._lastSelectedRowId = null;
this._checkedRowsChanged();
}
@@ -217,6 +222,7 @@ export class HaDataTable extends LitElement {
this._checkedRows.splice(index, 1);
}
});
+ this._lastSelectedRowId = null;
this._checkedRowsChanged();
}
@@ -261,6 +267,7 @@ export class HaDataTable extends LitElement {
if (this.columns[columnId].direction) {
this.sortDirection = this.columns[columnId].direction!;
this.sortColumn = columnId;
+ this._lastSelectedRowId = null;
fireEvent(this, "sorting-changed", {
column: columnId,
@@ -286,6 +293,7 @@ export class HaDataTable extends LitElement {
if (properties.has("filter")) {
this._debounceSearch(this.filter);
+ this._lastSelectedRowId = null;
}
if (properties.has("data")) {
@@ -296,9 +304,11 @@ export class HaDataTable extends LitElement {
if (!this.hasUpdated && this.initialCollapsedGroups) {
this._collapsedGroups = this.initialCollapsedGroups;
+ this._lastSelectedRowId = null;
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
} else if (properties.has("groupColumn")) {
this._collapsedGroups = [];
+ this._lastSelectedRowId = null;
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
}
@@ -312,6 +322,14 @@ export class HaDataTable extends LitElement {
this._sortFilterData();
}
+ if (
+ properties.has("_filter") ||
+ properties.has("sortColumn") ||
+ properties.has("sortDirection")
+ ) {
+ this._lastSelectedRowId = null;
+ }
+
if (properties.has("selectable") || properties.has("hiddenColumns")) {
this._filteredData = [...this._filteredData];
}
@@ -542,7 +560,7 @@ export class HaDataTable extends LitElement {
>
{
+ const collapsed = collapsedGroups.includes(groupName);
groupedItems.push({
append: true,
+ selectable: false,
content: html`