diff --git a/package.json b/package.json
index 204340c43269..4be38cc5707c 100644
--- a/package.json
+++ b/package.json
@@ -109,7 +109,7 @@
"element-internals-polyfill": "1.3.10",
"fuse.js": "7.0.0",
"google-timezones-json": "1.2.0",
- "hls.js": "1.5.2",
+ "hls.js": "1.5.3",
"home-assistant-js-websocket": "9.1.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.11",
@@ -183,8 +183,8 @@
"@types/tar": "6.1.11",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
- "@typescript-eslint/eslint-plugin": "6.19.1",
- "@typescript-eslint/parser": "6.19.1",
+ "@typescript-eslint/eslint-plugin": "6.20.0",
+ "@typescript-eslint/parser": "6.20.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
@@ -199,7 +199,7 @@
"eslint-plugin-disable": "2.0.3",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-lit": "1.11.0",
- "eslint-plugin-lit-a11y": "4.1.1",
+ "eslint-plugin-lit-a11y": "4.1.2",
"eslint-plugin-unused-imports": "3.0.0",
"eslint-plugin-wc": "2.0.4",
"fancy-log": "2.0.0",
@@ -212,7 +212,7 @@
"gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.1",
"html-minifier-terser": "7.2.0",
- "husky": "9.0.6",
+ "husky": "9.0.7",
"instant-mocha": "1.5.2",
"jszip": "3.10.1",
"lint-staged": "15.2.0",
diff --git a/pyproject.toml b/pyproject.toml
index c1f29c43b5ef..03402285c70b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
-version = "20240131.0"
+version = "20240202.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
diff --git a/src/common/util/compute_rtl.ts b/src/common/util/compute_rtl.ts
index 0c53e374e902..b0b8b20cf555 100644
--- a/src/common/util/compute_rtl.ts
+++ b/src/common/util/compute_rtl.ts
@@ -38,4 +38,8 @@ export function setDirectionStyles(direction: string, element: LitElement) {
"--margin-title",
direction === "ltr" ? "var(--margin-title-ltr)" : "var(--margin-title-rtl)"
);
+ element.style.setProperty(
+ "--scale-direction",
+ direction === "ltr" ? "1" : "-1"
+ );
}
diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts
index 36ddc2915c00..aa4a90fef8d6 100644
--- a/src/components/chart/ha-chart-base.ts
+++ b/src/components/chart/ha-chart-base.ts
@@ -10,7 +10,6 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { clamp } from "../../common/number/clamp";
-import { computeRTL } from "../../common/util/compute_rtl";
import { HomeAssistant } from "../../types";
import { debounce } from "../../common/util/debounce";
@@ -212,12 +211,10 @@ export class HaChartBase extends LitElement {
height: `${
this.height ?? this._chartHeight ?? this.clientWidth / 2
}px`,
- "padding-left": `${
- computeRTL(this.hass) ? 0 : this._paddingYAxisInternal
- }px`,
- "padding-right": `${
- computeRTL(this.hass) ? this._paddingYAxisInternal : 0
- }px`,
+ "padding-left": `${this._paddingYAxisInternal}`,
+ "padding-right": 0,
+ "padding-inline-start": `${this._paddingYAxisInternal}`,
+ "padding-inline-end": 0,
})}
>
@@ -433,14 +430,6 @@ export class HaChartBase extends LitElement {
.chartTooltip .bullet {
align-self: baseline;
}
- :host([rtl]) .chartLegend .bullet,
- :host([rtl]) .chartTooltip .bullet {
- margin-right: inherit;
- margin-left: 6px;
- margin-inline-end: inherit;
- margin-inline-start: 6px;
- direction: var(--direction);
- }
.chartTooltip {
padding: 8px;
font-size: 90%;
@@ -449,12 +438,13 @@ export class HaChartBase extends LitElement {
color: white;
border-radius: 4px;
pointer-events: none;
- z-index: 1000;
+ z-index: 1;
+ -ms-user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
width: 200px;
box-sizing: border-box;
- }
- :host([rtl]) .chartTooltip {
- direction: rtl;
+ direction: var(--direction);
}
.chartLegend ul,
.chartTooltip ul {
diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts
index 78ec773719c3..5f6286ecef61 100644
--- a/src/components/chart/state-history-chart-line.ts
+++ b/src/components/chart/state-history-chart-line.ts
@@ -220,7 +220,12 @@ export class StateHistoryChartLine extends LitElement {
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => {
- if (!this.clickForMoreInfo) {
+ if (
+ !this.clickForMoreInfo ||
+ !(e.native instanceof MouseEvent) ||
+ (e.native instanceof PointerEvent &&
+ e.native.pointerType !== "mouse")
+ ) {
return;
}
diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts
index 79b51be40ed3..8bfb0fef8b7f 100644
--- a/src/components/chart/state-history-chart-timeline.ts
+++ b/src/components/chart/state-history-chart-timeline.ts
@@ -224,7 +224,11 @@ export class StateHistoryChartTimeline extends LitElement {
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => {
- if (!this.clickForMoreInfo) {
+ if (
+ !this.clickForMoreInfo ||
+ !(e.native instanceof MouseEvent) ||
+ (e.native instanceof PointerEvent && e.native.pointerType !== "mouse")
+ ) {
return;
}
diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts
index 7f3d2f61b89b..c4805144b165 100644
--- a/src/components/data-table/ha-data-table.ts
+++ b/src/components/data-table/ha-data-table.ts
@@ -688,14 +688,11 @@ export class HaDataTable extends LitElement {
padding-left: 16px;
/* @noflip */
padding-right: 0;
- width: 60px;
- }
- :host([dir="rtl"]) .mdc-data-table__header-cell--checkbox,
- :host([dir="rtl"]) .mdc-data-table__cell--checkbox {
/* @noflip */
- padding-left: 0;
+ padding-inline-start: 16px;
/* @noflip */
- padding-right: 16px;
+ padding-inline-end: initial;
+ width: 60px;
}
.mdc-data-table__table {
@@ -723,11 +720,7 @@ export class HaDataTable extends LitElement {
}
.mdc-data-table__cell--numeric {
- text-align: right;
- }
- :host([dir="rtl"]) .mdc-data-table__cell--numeric {
- /* @noflip */
- text-align: left;
+ text-align: var(--float-end);
}
.mdc-data-table__cell--icon {
@@ -753,15 +746,7 @@ export class HaDataTable extends LitElement {
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:not(
.not-sorted
) {
- text-align: left;
- }
- :host([dir="rtl"])
- .mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:hover,
- :host([dir="rtl"])
- .mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:not(
- .not-sorted
- ) {
- text-align: right;
+ text-align: var(--float-start);
}
.mdc-data-table__cell--icon:first-child img,
@@ -771,27 +756,14 @@ export class HaDataTable extends LitElement {
.mdc-data-table__cell--icon:first-child ha-domain-icon,
.mdc-data-table__cell--icon:first-child ha-service-icon {
margin-left: 8px;
- }
- :host([dir="rtl"]) .mdc-data-table__cell--icon:first-child ha-icon,
- :host([dir="rtl"])
- .mdc-data-table__cell--icon:first-child
- ha-state-icon,
- :host([dir="rtl"])
- .mdc-data-table__cell--icon:first-child
- ha-svg-icon
- :host([dir="rtl"])
- .mdc-data-table__cell--icon:first-child
- img {
- margin-left: auto;
- margin-right: 8px;
+ margin-inline-start: 8px;
+ margin-inline-end: initial;
}
.mdc-data-table__cell--icon:first-child state-badge {
margin-right: -8px;
- }
- :host([dir="rtl"]) .mdc-data-table__cell--icon:first-child state-badge {
- margin-right: auto;
- margin-left: -8px;
+ margin-inline-end: -8px;
+ margin-inline-start: initial;
}
.mdc-data-table__cell--overflow-menu,
@@ -824,15 +796,8 @@ export class HaDataTable extends LitElement {
.mdc-data-table__header-cell--icon-button:first-child,
.mdc-data-table__cell--icon-button:first-child {
padding-left: 16px;
- }
- :host([dir="rtl"])
- .mdc-data-table__header-cell--overflow-menu:first-child,
- :host([dir="rtl"]) .mdc-data-table__cell--overflow-menu:first-child,
- :host([dir="rtl"])
- .mdc-data-table__header-cell--overflow-menu:first-child,
- :host([dir="rtl"]) .mdc-data-table__cell--overflow-menu:first-child {
- padding-left: 8px;
- padding-right: 16px;
+ padding-inline-start: 16px;
+ padding-inline-end: initial; // 8px?
}
.mdc-data-table__cell--overflow-menu:last-child,
@@ -840,14 +805,8 @@ export class HaDataTable extends LitElement {
.mdc-data-table__header-cell--icon-button:last-child,
.mdc-data-table__cell--icon-button:last-child {
padding-right: 16px;
- }
- :host([dir="rtl"])
- .mdc-data-table__header-cell--overflow-menu:last-child,
- :host([dir="rtl"]) .mdc-data-table__cell--overflow-menu:last-child,
- :host([dir="rtl"]) .mdc-data-table__header-cell--icon-button:last-child,
- :host([dir="rtl"]) .mdc-data-table__cell--icon-button:last-child {
- padding-right: 8px;
- padding-left: 16px;
+ padding-inline-end: 16px;
+ padding-inline-start: initial; // 8px?
}
.mdc-data-table__cell--overflow-menu,
.mdc-data-table__header-cell--overflow-menu {
@@ -867,28 +826,15 @@ export class HaDataTable extends LitElement {
letter-spacing: 0.0071428571em;
text-decoration: inherit;
text-transform: inherit;
- text-align: left;
- }
- :host([dir="rtl"]) .mdc-data-table__header-cell {
- /* @noflip */
- text-align: right;
+ text-align: var(--float-start);
}
.mdc-data-table__header-cell--numeric {
- text-align: right;
+ text-align: var(--float-end);
}
.mdc-data-table__header-cell--numeric.sortable:hover,
.mdc-data-table__header-cell--numeric.sortable:not(.not-sorted) {
- text-align: left;
- }
- :host([dir="rtl"]) .mdc-data-table__header-cell--numeric {
- /* @noflip */
- text-align: left;
- }
- :host([dir="rtl"]) .mdc-data-table__header-cell--numeric.sortable:hover,
- :host([dir="rtl"])
- .mdc-data-table__header-cell--numeric.sortable:not(.not-sorted) {
- text-align: right;
+ text-align: var(--float-start);
}
/* custom from here */
@@ -909,20 +855,15 @@ export class HaDataTable extends LitElement {
.mdc-data-table__header-cell span {
position: relative;
left: 0px;
- }
- :host([dir="rtl"]) .mdc-data-table__header-cell span {
- left: auto;
- right: 0px;
+ inset-inline-start: 0px;
+ inset-inline-end: initial;
}
.mdc-data-table__header-cell.sortable {
cursor: pointer;
}
.mdc-data-table__header-cell > * {
- transition: left 0.2s ease;
- }
- :host([dir="rtl"]) .mdc-data-table__header-cell > * {
- transition: right 0.2s ease;
+ transition: var(--float-start) 0.2s ease;
}
.mdc-data-table__header-cell ha-svg-icon {
top: -3px;
@@ -930,35 +871,20 @@ export class HaDataTable extends LitElement {
}
.mdc-data-table__header-cell.not-sorted ha-svg-icon {
left: -20px;
- }
- :host([dir="rtl"]) .mdc-data-table__header-cell.not-sorted ha-svg-icon {
- right: -20px;
+ inset-inline-start: -20px;
+ inset-inline-end: initial;
}
.mdc-data-table__header-cell.sortable:not(.not-sorted) span,
.mdc-data-table__header-cell.sortable.not-sorted:hover span {
left: 24px;
- }
- :host([dir="rtl"])
- .mdc-data-table__header-cell.sortable:not(.not-sorted)
- span,
- :host([dir="rtl"])
- .mdc-data-table__header-cell.sortable.not-sorted:hover
- span {
- left: auto;
- right: 24px;
+ inset-inline-start: 24px;
+ inset-inline-end: initial;
}
.mdc-data-table__header-cell.sortable:not(.not-sorted) ha-svg-icon,
.mdc-data-table__header-cell.sortable:hover.not-sorted ha-svg-icon {
left: 12px;
- }
- :host([dir="rtl"])
- .mdc-data-table__header-cell.sortable:not(.not-sorted)
- ha-svg-icon,
- :host([dir="rtl"])
- .mdc-data-table__header-cell.sortable:hover.not-sorted
- ha-svg-icon {
- left: auto;
- right: 12px;
+ inset-inline-start: 12px;
+ inset-inline-end: initial;
}
.table-header {
border-bottom: 1px solid var(--divider-color);
@@ -966,6 +892,8 @@ export class HaDataTable extends LitElement {
search-input {
display: block;
flex: 1;
+ --mdc-text-field-fill-color: var(--sidebar-background-color);
+ --mdc-text-field-idle-line-color: transparent;
}
slot[name="header"] {
display: block;
diff --git a/src/components/date-range-picker.ts b/src/components/date-range-picker.ts
index 237576a89ce3..5ebb0c2a978b 100644
--- a/src/components/date-range-picker.ts
+++ b/src/components/date-range-picker.ts
@@ -9,6 +9,7 @@ import {
localizeWeekdays,
localizeMonths,
} from "../common/datetime/localize_date";
+import { mainWindow } from "../common/dom/get_main_window";
// Set the current date to the left picker instead of the right picker because the right is hidden
const CustomDateRangePicker = Vue.extend({
@@ -157,7 +158,7 @@ class DateRangePickerElement extends WrappedElement {
min-width: initial !important;
max-height: var(--date-range-picker-max-height);
overflow-y: auto;
- }
+ }
.daterangepicker:before {
display: none;
}
@@ -267,15 +268,37 @@ class DateRangePickerElement extends WrappedElement {
.calendar-table {
padding: 0 !important;
}
- .daterangepicker.ltr {
+ .calendar-time {
direction: ltr;
- text-align: left;
+ }
+ .daterangepicker.ltr {
+ direction: var(--direction);
+ text-align: var(--float-start);
}
.vue-daterange-picker{
min-width: unset !important;
display: block !important;
}
`;
+ if (mainWindow.document.dir === "rtl") {
+ style.innerHTML += `
+ .daterangepicker .calendar-table .next span {
+ transform: rotate(135deg);
+ -webkit-transform: rotate(135deg);
+ }
+ .daterangepicker .calendar-table .prev span {
+ transform: rotate(-45deg);
+ -webkit-transform: rotate(-45deg);
+ }
+ .daterangepicker td.start-date {
+ border-radius: 0 50% 50% 0;
+ }
+ .daterangepicker td.end-date {
+ border-radius: 50% 0 0 50%;
+ }
+ `;
+ }
+
const shadowRoot = this.shadowRoot!;
shadowRoot.appendChild(style);
// Stop click events from reaching the document, otherwise it will close the picker immediately.
diff --git a/src/components/entity/state-info.ts b/src/components/entity/state-info.ts
index db631e4496f3..18bc50068c13 100644
--- a/src/components/entity/state-info.ts
+++ b/src/components/entity/state-info.ts
@@ -3,7 +3,6 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { computeStateName } from "../../common/entity/compute_state_name";
-import { computeRTL } from "../../common/util/compute_rtl";
import type { HomeAssistant } from "../../types";
import "../ha-relative-time";
import "./state-badge";
@@ -16,9 +15,6 @@ class StateInfo extends LitElement {
@property({ type: Boolean }) public inDialog = false;
- // property used only in CSS
- @property({ type: Boolean, reflect: true }) public rtl = false;
-
@property() public color?: string;
protected render() {
@@ -79,18 +75,6 @@ class StateInfo extends LitElement {
`;
}
- protected updated(changedProps) {
- super.updated(changedProps);
- if (!changedProps.has("hass")) {
- return;
- }
-
- const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
- if (!oldHass || oldHass.locale !== this.hass.locale) {
- this.rtl = computeRTL(this.hass);
- }
- }
-
static get styles(): CSSResultGroup {
return css`
:host {
@@ -106,17 +90,14 @@ class StateInfo extends LitElement {
.info {
margin-left: 8px;
+ margin-inline-start: 8px;
+ margin-inline-end: initial;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
min-width: 0;
- }
-
- :host([rtl]) .info {
- margin-right: 8px;
- margin-left: 0;
- text-align: right;
+ text-align: var(--float-start);
}
.name {
diff --git a/src/components/ha-button-toggle-group.ts b/src/components/ha-button-toggle-group.ts
index e4dd2e9524dd..9fc426a99176 100644
--- a/src/components/ha-button-toggle-group.ts
+++ b/src/components/ha-button-toggle-group.ts
@@ -69,6 +69,7 @@ export class HaButtonToggleGroup extends LitElement {
display: flex;
--mdc-icon-button-size: var(--button-toggle-size, 36px);
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
+ direction: ltr;
}
mwc-button {
--mdc-shape-small: 0;
@@ -119,19 +120,6 @@ export class HaButtonToggleGroup extends LitElement {
--mdc-shape-small: 4px;
border-right-width: 1px;
}
-
- :host([dir="rtl"]) ha-icon-button:first-child,
- :host([dir="rtl"]) mwc-button:first-child {
- border-radius: 0 4px 4px 0;
- border-right-width: 1px;
- --mdc-shape-small: 0 4px 4px 0;
- --mdc-button-outline-width: 1px;
- }
- :host([dir="rtl"]) ha-icon-button:last-child,
- :host([dir="rtl"]) mwc-button:last-child {
- --mdc-shape-small: 4px 0 0 4px;
- border-radius: 4px 0 0 4px;
- }
`;
}
}
diff --git a/src/components/ha-date-range-picker.ts b/src/components/ha-date-range-picker.ts
index 6984dc13fc61..1bb96721fc0d 100644
--- a/src/components/ha-date-range-picker.ts
+++ b/src/components/ha-date-range-picker.ts
@@ -32,7 +32,6 @@ import { firstWeekdayIndex } from "../common/datetime/first_weekday";
import { formatDate } from "../common/datetime/format_date";
import { formatDateTime } from "../common/datetime/format_date_time";
import { useAmPm } from "../common/datetime/use_am_pm";
-import { computeRTLDirection } from "../common/util/compute_rtl";
import { HomeAssistant } from "../types";
import "./date-range-picker";
import "./ha-icon-button";
@@ -65,8 +64,6 @@ export class HaDateRangePicker extends LitElement {
@state() private _hour24format = false;
- @state() private _rtlDirection = "ltr";
-
@property({ type: Boolean }) public extendedPresets = false;
@property() public openingDirection?: "right" | "left" | "center" | "inline";
@@ -236,7 +233,6 @@ export class HaDateRangePicker extends LitElement {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.locale !== this.hass.locale) {
this._hour24format = !useAmPm(this.hass.locale);
- this._rtlDirection = computeRTLDirection(this.hass);
}
}
}
@@ -306,11 +302,7 @@ export class HaDateRangePicker extends LitElement {
>`}
${this.ranges !== false && (this.ranges || this._ranges)
- ? html`
+ ? html`
${Object.keys(this.ranges || this._ranges!).map(
(name) => html`${name}`
diff --git a/src/components/ha-drawer.ts b/src/components/ha-drawer.ts
index 37461fb1dfaf..45a54623d56c 100644
--- a/src/components/ha-drawer.ts
+++ b/src/components/ha-drawer.ts
@@ -3,6 +3,7 @@ import { styles } from "@material/mwc-drawer/mwc-drawer.css";
import { css, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
+import { mainWindow } from "../common/dom/get_main_window";
const blockingElements = (document as any).$blockingElements;
@@ -12,6 +13,8 @@ export class HaDrawer extends DrawerBase {
private _mc?: HammerManager;
+ private _rtlStyle?: HTMLElement;
+
protected createAdapter() {
return {
...super.createAdapter(),
@@ -31,8 +34,26 @@ export class HaDrawer extends DrawerBase {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("direction")) {
- this.mdcRoot.dir = this.direction;
+ if (mainWindow.document.dir === "rtl") {
+ this._rtlStyle = document.createElement("style");
+ this._rtlStyle.innerHTML = `
+ .mdc-drawer--animate {
+ transform: translateX(100%);
+ }
+ .mdc-drawer--opening {
+ transform: translateX(0);
+ }
+ .mdc-drawer--closing {
+ transform: translateX(100%);
+ }
+ `;
+
+ this.shadowRoot!.appendChild(this._rtlStyle);
+ } else if (this._rtlStyle) {
+ this.shadowRoot!.removeChild(this._rtlStyle);
+ }
}
+
if (changedProps.has("open") && this.open && this.type === "modal") {
this._setupSwipe();
} else if (this._mc) {
@@ -66,6 +87,8 @@ export class HaDrawer extends DrawerBase {
position: fixed;
top: 0;
border-color: var(--divider-color, rgba(0, 0, 0, 0.12));
+ inset-inline-start: 0 !important;
+ inset-inline-end: initial !important;
}
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
z-index: 200;
diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts
index ba199581707a..d36a6a646db0 100644
--- a/src/components/ha-sidebar.ts
+++ b/src/components/ha-sidebar.ts
@@ -38,7 +38,6 @@ import { storage } from "../common/decorators/storage";
import { fireEvent } from "../common/dom/fire_event";
import { toggleAttribute } from "../common/dom/toggle_attribute";
import { stringCompare } from "../common/string/compare";
-import { computeRTL } from "../common/util/compute_rtl";
import { throttle } from "../common/util/throttle";
import { ActionHandlerDetail } from "../data/lovelace/action_handler";
import {
@@ -307,16 +306,12 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return;
}
- const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
- if (!oldHass || oldHass.locale !== this.hass.locale) {
- toggleAttribute(this, "rtl", computeRTL(this.hass));
- }
-
this._calculateCounts();
if (!SUPPORT_SCROLL_IF_NEEDED) {
return;
}
+ const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.panelUrl !== this.hass.panelUrl) {
const selectedEl = this.shadowRoot!.querySelector(".iron-selected");
if (selectedEl) {
@@ -851,29 +846,22 @@ class HaSidebar extends SubscribeMixin(LitElement) {
font-size: 20px;
align-items: center;
padding-left: calc(4px + env(safe-area-inset-left));
- }
- :host([rtl]) .menu {
- padding-left: 4px;
- padding-right: calc(4px + env(safe-area-inset-right));
+ padding-inline-start: calc(4px + env(safe-area-inset-left));
+ padding-inline-end: initial;
}
:host([expanded]) .menu {
width: calc(256px + env(safe-area-inset-left));
}
- :host([rtl][expanded]) .menu {
- width: calc(256px + env(safe-area-inset-right));
- }
.menu ha-icon-button {
color: var(--sidebar-icon-color);
}
.title {
margin-left: 19px;
+ margin-inline-start: 19px;
+ margin-inline-end: initial;
width: 100%;
display: none;
}
- :host([rtl]) .title {
- margin-left: 0;
- margin-right: 19px;
- }
:host([narrow]) .title {
margin: 0;
padding: 0 16px;
@@ -904,11 +892,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
overflow-x: hidden;
background: none;
margin-left: env(safe-area-inset-left);
- }
-
- :host([rtl]) paper-listbox {
- margin-left: initial;
- margin-right: env(safe-area-inset-right);
+ margin-inline-start: env(safe-area-inset-left);
+ margin-inline-end: initial;
}
a {
@@ -925,6 +910,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
box-sizing: border-box;
margin: 4px;
padding-left: 12px;
+ padding-inline-start: 12px;
+ padding-inline-end: initial;
border-radius: 4px;
--paper-item-min-height: 40px;
width: 48px;
@@ -932,10 +919,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
:host([expanded]) paper-icon-item {
width: 248px;
}
- :host([rtl]) paper-icon-item {
- padding-left: auto;
- padding-right: 12px;
- }
ha-icon[slot="item-icon"],
ha-svg-icon[slot="item-icon"] {
@@ -1010,11 +993,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
.configuration-container {
display: flex;
margin-left: env(safe-area-inset-left);
- }
- :host([rtl]) .notifications-container,
- :host([rtl]) .configuration-container {
- margin-left: initial;
- margin-right: env(safe-area-inset-right);
+ margin-inline-start: env(safe-area-inset-left);
+ margin-inline-end: initial;
}
.notifications {
cursor: pointer;
@@ -1025,23 +1005,18 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
.profile {
margin-left: env(safe-area-inset-left);
- }
- :host([rtl]) .profile {
- margin-left: initial;
- margin-right: env(safe-area-inset-right);
+ margin-inline-start: env(safe-area-inset-left);
+ margin-inline-end: initial;
}
.profile paper-icon-item {
padding-left: 4px;
- }
- :host([rtl]) .profile paper-icon-item {
- padding-left: auto;
- padding-right: 4px;
+ margin-inline-start: 4px;
+ margin-inline-end: auto;
}
.profile .item-text {
margin-left: 8px;
- }
- :host([rtl]) .profile .item-text {
- margin-right: 8px;
+ margin-inline-start: 8px;
+ margin-inline-end: initial;
}
.notification-badge,
@@ -1106,9 +1081,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
font-weight: 500;
}
- :host([rtl]) .menu ha-icon-button {
- -webkit-transform: scaleX(-1);
- transform: scaleX(-1);
+ .menu ha-icon-button {
+ -webkit-transform: scaleX(var(--scale-direction));
+ transform: scaleX(var(--scale-direction));
}
`,
];
diff --git a/src/components/ha-slider.ts b/src/components/ha-slider.ts
index 5ea8d50b3321..31607a62f568 100644
--- a/src/components/ha-slider.ts
+++ b/src/components/ha-slider.ts
@@ -2,9 +2,15 @@ import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { MdSlider } from "@material/web/slider/slider";
import { CSSResult, css } from "lit";
+import { mainWindow } from "../common/dom/get_main_window";
@customElement("ha-slider")
export class HaSlider extends MdSlider {
+ public connectedCallback() {
+ super.connectedCallback();
+ this.dir = mainWindow.document.dir;
+ }
+
static override styles: CSSResult[] = [
...MdSlider.styles,
css`
diff --git a/src/components/ha-sortable.ts b/src/components/ha-sortable.ts
index b188620c87d0..5258c2fc5fb6 100644
--- a/src/components/ha-sortable.ts
+++ b/src/components/ha-sortable.ts
@@ -39,6 +39,12 @@ export class HaSortable extends LitElement {
@property({ type: String, attribute: "group" })
public group?: string;
+ @property({ type: Number, attribute: "swap-threshold" })
+ public swapThreshold?: number;
+
+ @property({ type: Boolean, attribute: "invert-swap" })
+ public invertSwap?: boolean;
+
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("disabled")) {
if (this.disabled) {
@@ -81,7 +87,7 @@ export class HaSortable extends LitElement {
}
.sortable-ghost {
- border: 2px solid var(--primary-color);
+ box-shadow: 0 0 0 2px var(--primary-color);
background: rgba(var(--rgb-primary-color), 0.25);
border-radius: 4px;
opacity: 0.4;
@@ -108,7 +114,7 @@ export class HaSortable extends LitElement {
const options: SortableInstance.Options = {
animation: 150,
- swapThreshold: 0.75,
+ swapThreshold: 1,
onChoose: this._handleChoose,
onEnd: this._handleEnd,
};
@@ -116,6 +122,13 @@ export class HaSortable extends LitElement {
if (this.draggableSelector) {
options.draggable = this.draggableSelector;
}
+
+ if (this.swapThreshold !== undefined) {
+ options.swapThreshold = this.swapThreshold;
+ }
+ if (this.invertSwap !== undefined) {
+ options.invertSwap = this.invertSwap;
+ }
if (this.handleSelector) {
options.handle = this.handleSelector;
}
diff --git a/src/components/ha-textarea.ts b/src/components/ha-textarea.ts
index 7cf773b90312..0fef6fe5a0b3 100644
--- a/src/components/ha-textarea.ts
+++ b/src/components/ha-textarea.ts
@@ -3,18 +3,11 @@ import { styles as textfieldStyles } from "@material/mwc-textfield/mwc-textfield
import { styles as textareaStyles } from "@material/mwc-textarea/mwc-textarea.css";
import { css, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
-import { mainWindow } from "../common/dom/get_main_window";
@customElement("ha-textarea")
export class HaTextArea extends TextAreaBase {
@property({ type: Boolean, reflect: true }) autogrow = false;
- firstUpdated() {
- super.firstUpdated();
-
- this.setAttribute("dir", mainWindow.document.dir);
- }
-
updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (this.autogrow && changedProperties.has("value")) {
@@ -54,9 +47,10 @@ export class HaTextArea extends TextAreaBase {
margin-top: 16px;
margin-bottom: 16px;
}
- :host([dir="rtl"]) .mdc-floating-label {
- right: 16px;
- left: initial;
+ .mdc-floating-label {
+ inset-inline-start: 16px !important;
+ inset-inline-end: initial !important;
+ transform-origin: var(--float-start) top;
}
`,
];
diff --git a/src/components/map/ha-map.ts b/src/components/map/ha-map.ts
index 42834dbe6907..1fcbceace41b 100644
--- a/src/components/map/ha-map.ts
+++ b/src/components/map/ha-map.ts
@@ -8,12 +8,18 @@ import type {
Marker,
Polyline,
} from "leaflet";
+import { isToday } from "date-fns";
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
LeafletModuleType,
setupLeafletMap,
} from "../../common/dom/setup-leaflet-map";
+import {
+ formatTimeWithSeconds,
+ formatTimeWeekday,
+} from "../../common/datetime/format_time";
+import { formatDateTime } from "../../common/datetime/format_date_time";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { loadPolyfillIfNeeded } from "../../resources/resize-observer.polyfill";
@@ -27,12 +33,14 @@ const getEntityId = (entity: string | HaMapEntity): string =>
export interface HaMapPathPoint {
point: LatLngTuple;
- tooltip: string;
+ timestamp: Date;
}
export interface HaMapPaths {
points: HaMapPathPoint[];
color?: string;
+ name?: string;
gradualOpacity?: number;
+ fullDatetime?: boolean;
}
export interface HaMapEntity {
@@ -242,6 +250,30 @@ export class HaMap extends ReactiveElement {
});
}
+ private _computePathTooltip(path: HaMapPaths, point: HaMapPathPoint): string {
+ let formattedTime: string;
+ if (path.fullDatetime) {
+ formattedTime = formatDateTime(
+ point.timestamp,
+ this.hass.locale,
+ this.hass.config
+ );
+ } else if (isToday(point.timestamp)) {
+ formattedTime = formatTimeWithSeconds(
+ point.timestamp,
+ this.hass.locale,
+ this.hass.config
+ );
+ } else {
+ formattedTime = formatTimeWeekday(
+ point.timestamp,
+ this.hass.locale,
+ this.hass.config
+ );
+ }
+ return `${path.name}
${formattedTime}`;
+ }
+
private _drawPaths(): void {
const hass = this.hass;
const map = this.leafletMap;
@@ -289,7 +321,10 @@ export class HaMap extends ReactiveElement {
fillOpacity: opacity,
interactive: true,
})
- .bindTooltip(path.points[pointIndex].tooltip, { direction: "top" })
+ .bindTooltip(
+ this._computePathTooltip(path, path.points[pointIndex]),
+ { direction: "top" }
+ )
);
// DRAW line between this and next point
@@ -319,7 +354,10 @@ export class HaMap extends ReactiveElement {
fillOpacity: opacity,
interactive: true,
})
- .bindTooltip(path.points[pointIndex].tooltip, { direction: "top" })
+ .bindTooltip(
+ this._computePathTooltip(path, path.points[pointIndex]),
+ { direction: "top" }
+ )
);
}
this._mapPaths.forEach((marker) => map.addLayer(marker));
@@ -556,6 +594,7 @@ export class HaMap extends ReactiveElement {
color: white !important;
border-radius: 4px;
box-shadow: none !important;
+ text-align: center;
}
`;
}
diff --git a/src/components/media-player/ha-media-player-browse.ts b/src/components/media-player/ha-media-player-browse.ts
index 6737ea613ba2..e46c9e908c43 100644
--- a/src/components/media-player/ha-media-player-browse.ts
+++ b/src/components/media-player/ha-media-player-browse.ts
@@ -25,7 +25,6 @@ import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event";
-import { computeRTLDirection } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce";
import { isUnavailableState } from "../../data/entity";
import type { MediaPlayerItem } from "../../data/media-player";
@@ -539,7 +538,6 @@ export class HaMediaPlayerBrowse extends LitElement {
.graphic=${mediaClass.show_list_images
? "medium"
: "avatar"}
- dir=${computeRTLDirection(this.hass)}
>
${this.hass.localize(
@@ -637,7 +635,6 @@ export class HaMediaPlayerBrowse extends LitElement {
@click=${this._childClicked}
.item=${child}
.graphic=${mediaClass.show_list_images ? "medium" : "avatar"}
- dir=${computeRTLDirection(this.hass)}
>
${backgroundImage === "none" && !child.can_play
? html` = {
+const resources: {
+ entity: Record>;
+ entity_component: {
+ domains?: string[];
+ resources?: Promise>;
+ };
+ services: {
+ all?: Promise>;
+ domains: { [domain: string]: ServiceIcons | Promise };
+ };
+} = {
entity: {},
- entity_component: undefined,
- services: {},
+ entity_component: {},
+ services: { domains: {} },
};
-interface IconResources {
- resources: Record>;
+interface IconResources<
+ T extends ComponentIcons | PlatformIcons | ServiceIcons,
+> {
+ resources: Record;
}
interface PlatformIcons {
- [domain: string]: {
- [translation_key: string]: {
- state: Record;
- state_attributes: Record<
- string,
- {
- state: Record;
- default: string;
- }
- >;
- default: string;
- };
+ [translation_key: string]: {
+ state: Record;
+ state_attributes: Record<
+ string,
+ {
+ state: Record;
+ default: string;
+ }
+ >;
+ default: string;
};
}
@@ -55,12 +66,18 @@ interface ServiceIcons {
export type IconCategory = "entity" | "entity_component" | "services";
-export const getHassIcons = async (
+type CategoryType = {
+ entity: PlatformIcons;
+ entity_component: ComponentIcons;
+ services: ServiceIcons;
+};
+
+export const getHassIcons = async (
hass: HomeAssistant,
- category: IconCategory,
+ category: T,
integration?: string
-): Promise =>
- hass.callWS<{ resources: Record }>({
+) =>
+ hass.callWS>({
type: "frontend/get_icons",
category,
integration,
@@ -70,14 +87,17 @@ export const getPlatformIcons = async (
hass: HomeAssistant,
integration: string,
force = false
-): Promise => {
+): Promise => {
if (!force && integration in resources.entity) {
return resources.entity[integration];
}
- const result = getHassIcons(hass, "entity", integration);
- resources.entity[integration] = result.then(
+ if (!isComponentLoaded(hass, integration)) {
+ return undefined;
+ }
+ const result = getHassIcons(hass, "entity", integration).then(
(res) => res?.resources[integration]
);
+ resources.entity[integration] = result;
return resources.entity[integration];
};
@@ -85,45 +105,59 @@ export const getComponentIcons = async (
hass: HomeAssistant,
domain: string,
force = false
-): Promise => {
- if (!force && resources.entity_component) {
- return resources.entity_component.then((res) => res[domain]);
+): Promise => {
+ if (
+ !force &&
+ resources.entity_component.resources &&
+ resources.entity_component.domains?.includes(domain)
+ ) {
+ return resources.entity_component.resources.then((res) => res[domain]);
}
- resources.entity_component = getHassIcons(hass, "entity_component").then(
- (result) => result.resources
- );
- return resources.entity_component.then((res) => res[domain]);
+ if (!isComponentLoaded(hass, domain)) {
+ return undefined;
+ }
+ resources.entity_component.domains = [...hass.config.components];
+ resources.entity_component.resources = getHassIcons(
+ hass,
+ "entity_component"
+ ).then((result) => result.resources);
+ return resources.entity_component.resources.then((res) => res[domain]);
};
export const getServiceIcons = async (
hass: HomeAssistant,
domain?: string,
force = false
-): Promise => {
+): Promise | undefined> => {
if (!domain) {
if (!force && resources.services.all) {
return resources.services.all;
}
resources.services.all = getHassIcons(hass, "services", domain).then(
(res) => {
- resources.services = res.resources;
+ resources.services.domains = res.resources;
return res?.resources;
}
);
return resources.services.all;
}
- if (!force && domain && domain in resources.services) {
- return resources.services[domain];
+ if (!force && domain in resources.services.domains) {
+ return resources.services.domains[domain];
}
if (resources.services.all && !force) {
await resources.services.all;
- if (domain in resources.services) {
- return resources.services[domain];
+ if (domain in resources.services.domains) {
+ return resources.services.domains[domain];
}
}
+ if (!isComponentLoaded(hass, domain)) {
+ return undefined;
+ }
const result = getHassIcons(hass, "services", domain);
- resources.services[domain] = result.then((res) => res?.resources[domain]);
- return resources.services[domain];
+ resources.services.domains[domain] = result.then(
+ (res) => res?.resources[domain]
+ );
+ return resources.services.domains[domain];
};
export const entityIcon = async (
@@ -238,7 +272,7 @@ export const serviceIcon = async (
const serviceName = computeObjectId(service);
const serviceIcons = await getServiceIcons(hass, domain);
if (serviceIcons) {
- icon = serviceIcons[serviceName];
+ icon = serviceIcons[serviceName] as string;
}
if (!icon) {
icon = await domainIcon(hass, domain);
diff --git a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts
index 7ed96ec06197..da3f8276dfa9 100644
--- a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts
+++ b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts
@@ -2,7 +2,6 @@ import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
-import { computeRTLDirection } from "../../common/util/compute_rtl";
import "../../components/ha-dialog";
import "../../components/ha-formfield";
import "../../components/ha-switch";
@@ -82,7 +81,6 @@ class DialogConfigEntrySystemOptions extends LitElement {
}
)}
`}
- .dir=${computeRTLDirection(this.hass)}
>
`}
- .dir=${computeRTLDirection(this.hass)}
>
diff --git a/src/layouts/hass-subpage.ts b/src/layouts/hass-subpage.ts
index 3a1ed243853a..f297a77b189d 100644
--- a/src/layouts/hass-subpage.ts
+++ b/src/layouts/hass-subpage.ts
@@ -1,15 +1,6 @@
-import {
- css,
- CSSResultGroup,
- html,
- LitElement,
- PropertyValues,
- TemplateResult,
-} from "lit";
+import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, eventOptions, property } from "lit/decorators";
import { restoreScroll } from "../common/decorators/restore-scroll";
-import { toggleAttribute } from "../common/dom/toggle_attribute";
-import { computeRTL } from "../common/util/compute_rtl";
import "../components/ha-icon-button-arrow-prev";
import "../components/ha-menu-button";
import { HomeAssistant } from "../types";
@@ -34,17 +25,6 @@ class HassSubpage extends LitElement {
// @ts-ignore
@restoreScroll(".content") private _savedScrollPos?: number;
- protected willUpdate(changedProps: PropertyValues): void {
- super.willUpdate(changedProps);
- if (!changedProps.has("hass")) {
- return;
- }
- const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
- if (!oldHass || oldHass.locale !== this.hass.locale) {
- toggleAttribute(this, "rtl", computeRTL(this.hass));
- }
- }
-
protected render(): TemplateResult {
return html`
@@ -160,6 +140,9 @@ class HassSubpage extends LitElement {
#fab {
position: absolute;
right: calc(16px + env(safe-area-inset-right));
+ inset-inline-end: calc(16px + env(safe-area-inset-right));
+ inset-inline-start: initial;
+
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
@@ -169,15 +152,8 @@ class HassSubpage extends LitElement {
#fab[is-wide] {
bottom: 24px;
right: 24px;
- }
- :host([rtl]) #fab {
- right: auto;
- left: calc(16px + env(safe-area-inset-left));
- }
- :host([rtl][is-wide]) #fab {
- bottom: 24px;
- left: 24px;
- right: auto;
+ inset-inline-end: 24px;
+ inset-inline-start: initial;
}
`,
];
diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts
index 275cfe82f12f..7af35ded290c 100644
--- a/src/layouts/hass-tabs-subpage-data-table.ts
+++ b/src/layouts/hass-tabs-subpage-data-table.ts
@@ -4,7 +4,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { LocalizeFunc } from "../common/translations/localize";
-import { computeRTLDirection } from "../common/util/compute_rtl";
import "../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
@@ -244,7 +243,6 @@ export class HaTabsSubpageDataTable extends LitElement {
.selectable=${this.selectable}
.hasFab=${this.hasFab}
.id=${this.id}
- .dir=${computeRTLDirection(this.hass)}
.clickable=${this.clickable}
.appendRow=${this.appendRow}
>
diff --git a/src/layouts/hass-tabs-subpage.ts b/src/layouts/hass-tabs-subpage.ts
index 15d8b2662927..9de45b8a4a21 100644
--- a/src/layouts/hass-tabs-subpage.ts
+++ b/src/layouts/hass-tabs-subpage.ts
@@ -13,7 +13,6 @@ import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { restoreScroll } from "../common/decorators/restore-scroll";
import { LocalizeFunc } from "../common/translations/localize";
-import { computeRTL } from "../common/util/compute_rtl";
import "../components/ha-icon-button-arrow-prev";
import "../components/ha-menu-button";
import "../components/ha-svg-icon";
@@ -58,8 +57,6 @@ class HassTabsSubpage extends LitElement {
@property({ type: Boolean, reflect: true, attribute: "is-wide" })
public isWide = false;
- @property({ type: Boolean, reflect: true }) public rtl = false;
-
@state() private _activeTab?: PageNavigation;
// @ts-ignore
@@ -123,14 +120,6 @@ class HassTabsSubpage extends LitElement {
`${this.route.prefix}${this.route.path}`.includes(tab.path)
);
}
- if (changedProperties.has("hass")) {
- const oldHass = changedProperties.get("hass") as
- | HomeAssistant
- | undefined;
- if (!oldHass || oldHass.language !== this.hass.language) {
- this.rtl = computeRTL(this.hass);
- }
- }
super.willUpdate(changedProperties);
}
@@ -334,6 +323,8 @@ class HassTabsSubpage extends LitElement {
#fab {
position: fixed;
right: calc(16px + env(safe-area-inset-right));
+ inset-inline-end: calc(16px + env(safe-area-inset-right));
+ inset-inline-start: initial;
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
@@ -343,15 +334,8 @@ class HassTabsSubpage extends LitElement {
#fab[is-wide] {
bottom: 24px;
right: 24px;
- }
- :host([rtl]) #fab {
- right: auto;
- left: calc(16px + env(safe-area-inset-left));
- }
- :host([rtl][is-wide]) #fab {
- bottom: 24px;
- left: 24px;
- right: auto;
+ inset-inline-end: 24px;
+ inset-inline-start: initial;
}
`,
];
diff --git a/src/layouts/home-assistant-main.ts b/src/layouts/home-assistant-main.ts
index d6e7b3c82648..29963ac45d16 100644
--- a/src/layouts/home-assistant-main.ts
+++ b/src/layouts/home-assistant-main.ts
@@ -11,7 +11,6 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent, HASSDomEvent } from "../common/dom/fire_event";
import { listenMediaQuery } from "../common/dom/media_query";
import { toggleAttribute } from "../common/dom/toggle_attribute";
-import { computeRTLDirection } from "../common/util/compute_rtl";
import "../components/ha-drawer";
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
import type { HomeAssistant, Route } from "../types";
@@ -62,7 +61,6 @@ export class HomeAssistantMain extends LitElement {
`
: html`
@@ -203,7 +201,6 @@ export class HAFullCalendar extends LitElement {
.buttons=${viewToggleButtons}
.active=${this._activeView}
@value-changed=${this._handleView}
- .dir=${computeRTLDirection(this.hass)}
>
`}
@@ -503,6 +500,8 @@ export class HAFullCalendar extends LitElement {
position: absolute;
bottom: 32px;
right: 32px;
+ inset-inline-end: 32px;
+ inset-inline-start: initial;
z-index: 1;
}
diff --git a/src/panels/config/automation/action/ha-automation-action.ts b/src/panels/config/automation/action/ha-automation-action.ts
index 3cf22ec221a2..fde289baccd5 100644
--- a/src/panels/config/automation/action/ha-automation-action.ts
+++ b/src/panels/config/automation/action/ha-automation-action.ts
@@ -81,6 +81,7 @@ export default class HaAutomationAction extends LitElement {
@item-moved=${this._actionMoved}
group="actions"
.path=${this.path}
+ invert-swap
>
${repeat(
@@ -266,22 +267,32 @@ export default class HaAutomationAction extends LitElement {
static get styles(): CSSResultGroup {
return css`
+ .actions {
+ padding: 16px;
+ margin: -16px -16px 0px -16px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+ .actions:not(:has(ha-automation-action-row)) {
+ margin: -16px;
+ }
+ .sortable-ghost {
+ background: none;
+ border-radius: var(--ha-card-border-radius, 12px);
+ }
+ .sortable-drag {
+ background: none;
+ }
ha-automation-action-row {
display: block;
- margin-bottom: 16px;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
- ha-alert {
- display: block;
- margin-bottom: 16px;
- border-radius: var(--ha-card-border-radius, 12px);
- overflow: hidden;
- }
.handle {
- padding: 12px 4px;
+ padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
diff --git a/src/panels/config/automation/action/types/ha-automation-action-choose.ts b/src/panels/config/automation/action/types/ha-automation-action-choose.ts
index 56dfe10da7ca..d57379ab8ed4 100644
--- a/src/panels/config/automation/action/types/ha-automation-action-choose.ts
+++ b/src/panels/config/automation/action/types/ha-automation-action-choose.ts
@@ -125,6 +125,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
.disabled=${!this._showReorder || this.disabled}
group="choose-options"
.path=${[...(this.path ?? []), "choose"]}
+ invert-swap
>
${repeat(
@@ -296,7 +297,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
)}:
${repeat(
@@ -291,22 +292,32 @@ export default class HaAutomationCondition extends LitElement {
static get styles(): CSSResultGroup {
return css`
+ .conditions {
+ padding: 16px;
+ margin: -16px -16px 0px -16px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+ .conditions:not(:has(ha-automation-condition-row)) {
+ margin: -16px;
+ }
+ .sortable-ghost {
+ background: none;
+ border-radius: var(--ha-card-border-radius, 12px);
+ }
+ .sortable-drag {
+ background: none;
+ }
ha-automation-condition-row {
display: block;
- margin-bottom: 16px;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
- ha-alert {
- display: block;
- margin-bottom: 16px;
- border-radius: var(--ha-card-border-radius, 12px);
- overflow: hidden;
- }
.handle {
- padding: 12px 4px;
+ padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts
index ed180172b3ee..04343c20ec16 100644
--- a/src/panels/config/automation/ha-automation-editor.ts
+++ b/src/panels/config/automation/ha-automation-editor.ts
@@ -1,5 +1,4 @@
import "@material/mwc-button";
-import "@material/mwc-list/mwc-list-item";
import {
mdiCheck,
mdiContentDuplicate,
@@ -35,6 +34,7 @@ import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
+import "../../../components/ha-list-item";
import {
AutomationConfig,
AutomationEntity,
@@ -150,7 +150,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
.path=${mdiDotsVertical}
>
-
-
+
-
${this.hass.localize("ui.panel.config.automation.editor.run")}
-
+
${stateObj && this._config && this.narrow
? html`
-
+
${this.hass.localize(
"ui.panel.config.automation.editor.show_trace"
)}
@@ -181,22 +181,22 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
slot="graphic"
.path=${mdiTransitConnection}
>
-
+
`
: ""}
-
${this.hass.localize("ui.panel.config.automation.editor.rename")}
-
+
${this._config && !("use_blueprint" in this._config)
? html`
-
-
+
`
: ""}
-
-
+
-
+
${this.hass.localize("ui.panel.config.automation.editor.edit_ui")}
${this._mode === "gui"
? html``
: ``}
-
-
+
+
${this.hass.localize("ui.panel.config.automation.editor.edit_yaml")}
${this._mode === "yaml"
? html``
: ``}
-
+
-
-
+
-
-
+
${this._config
diff --git a/src/panels/config/automation/trigger/ha-automation-trigger.ts b/src/panels/config/automation/trigger/ha-automation-trigger.ts
index 6de4247300ab..69041acc9287 100644
--- a/src/panels/config/automation/trigger/ha-automation-trigger.ts
+++ b/src/panels/config/automation/trigger/ha-automation-trigger.ts
@@ -78,6 +78,7 @@ export default class HaAutomationTrigger extends LitElement {
@item-moved=${this._triggerMoved}
group="triggers"
.path=${this.path}
+ invert-swap
>
${repeat(
@@ -240,22 +241,32 @@ export default class HaAutomationTrigger extends LitElement {
static get styles(): CSSResultGroup {
return css`
+ .triggers {
+ padding: 16px;
+ margin: -16px -16px 0px -16px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+ .triggers:not(:has(ha-automation-trigger-row)) {
+ margin: -16px;
+ }
+ .sortable-ghost {
+ background: none;
+ border-radius: var(--ha-card-border-radius, 12px);
+ }
+ .sortable-drag {
+ background: none;
+ }
ha-automation-trigger-row {
display: block;
- margin-bottom: 16px;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
- ha-alert {
- display: block;
- margin-bottom: 16px;
- border-radius: var(--ha-card-border-radius, 16px);
- overflow: hidden;
- }
.handle {
- padding: 12px 4px;
+ padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
diff --git a/src/panels/config/cloud/account/cloud-account.ts b/src/panels/config/cloud/account/cloud-account.ts
index 293576fa8f4e..8c81013cd24f 100644
--- a/src/panels/config/cloud/account/cloud-account.ts
+++ b/src/panels/config/cloud/account/cloud-account.ts
@@ -1,9 +1,8 @@
import "@material/mwc-button";
-import { css, html, LitElement, PropertyValues } from "lit";
+import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { formatDateTime } from "../../../../common/datetime/format_date_time";
import { fireEvent } from "../../../../common/dom/fire_event";
-import { computeRTLDirection } from "../../../../common/util/compute_rtl";
import { debounce } from "../../../../common/util/debounce";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
@@ -37,8 +36,6 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
@state() private _subscription?: SubscriptionInfo;
- @state() private _rtlDirection: "rtl" | "ltr" = "rtl";
-
protected render() {
return html`
@@ -193,7 +188,6 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.narrow=${this.narrow}
.cloudStatus=${this.cloudStatus}
- dir=${this._rtlDirection}
>
@@ -205,15 +199,6 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
this._fetchSubscriptionInfo();
}
- protected updated(changedProps: PropertyValues) {
- if (changedProps.has("hass")) {
- const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
- if (!oldHass || oldHass.locale !== this.hass.locale) {
- this._rtlDirection = computeRTLDirection(this.hass);
- }
- }
- }
-
protected override hassSubscribe() {
const googleCheck = debounce(
() => {
@@ -272,10 +257,6 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
fireEvent(this, "ha-refresh-cloud-status");
}
- _computeRTLDirection(hass) {
- return computeRTLDirection(hass);
- }
-
static get styles() {
return [
haStyle,
diff --git a/src/panels/config/cloud/account/cloud-remote-pref.ts b/src/panels/config/cloud/account/cloud-remote-pref.ts
index e5d0771b3932..bcfb4bdde797 100644
--- a/src/panels/config/cloud/account/cloud-remote-pref.ts
+++ b/src/panels/config/cloud/account/cloud-remote-pref.ts
@@ -179,14 +179,12 @@ export class CloudRemotePref extends LitElement {
.header-actions {
position: absolute;
right: 24px;
+ inset-inline-end: 24px;
+ inset-inline-start: initial;
top: 24px;
display: flex;
flex-direction: row;
}
- :host([dir="rtl"]) .header-actions {
- right: auto;
- left: 24px;
- }
.header-actions .icon-link {
margin-top: -16px;
margin-right: 8px;
diff --git a/src/panels/config/cloud/account/cloud-tts-pref.ts b/src/panels/config/cloud/account/cloud-tts-pref.ts
index 052fad179bf5..c1e85fccdc25 100644
--- a/src/panels/config/cloud/account/cloud-tts-pref.ts
+++ b/src/panels/config/cloud/account/cloud-tts-pref.ts
@@ -177,12 +177,10 @@ export class CloudTTSPref extends LitElement {
.example {
position: absolute;
right: 16px;
+ inset-inline-end: 16px;
+ inset-inline-start: initial;
top: 16px;
}
- :host([dir="rtl"]) .example {
- right: auto;
- left: 24px;
- }
.row {
display: flex;
}
diff --git a/src/panels/config/devices/device-detail/integration-elements/mqtt/dialog-mqtt-device-debug-info.ts b/src/panels/config/devices/device-detail/integration-elements/mqtt/dialog-mqtt-device-debug-info.ts
index 2a629114aa21..3884f65b5d97 100644
--- a/src/panels/config/devices/device-detail/integration-elements/mqtt/dialog-mqtt-device-debug-info.ts
+++ b/src/panels/config/devices/device-detail/integration-elements/mqtt/dialog-mqtt-device-debug-info.ts
@@ -9,7 +9,6 @@ import {
} from "lit";
import { customElement, state } from "lit/decorators";
import { computeStateName } from "../../../../../../common/entity/compute_state_name";
-import { computeRTLDirection } from "../../../../../../common/util/compute_rtl";
import "../../../../../../components/ha-dialog";
import "../../../../../../components/ha-formfield";
import "../../../../../../components/ha-switch";
@@ -51,8 +50,6 @@ class DialogMQTTDeviceDebugInfo extends LitElement {
return nothing;
}
- const dir = computeRTLDirection(this.hass!);
-
return html`
diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts
index 187a8a14efa6..0599d1e75e9d 100644
--- a/src/panels/config/entities/ha-config-entities.ts
+++ b/src/panels/config/entities/ha-config-entities.ts
@@ -35,7 +35,6 @@ import {
} from "../../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
-import { computeRTL } from "../../../common/util/compute_rtl";
import type {
DataTableColumnContainer,
RowClickedEvent,
@@ -680,7 +679,6 @@ export class HaConfigEntities extends LitElement {
extended
@click=${this._addDevice}
slot="fab"
- ?rtl=${computeRTL(this.hass)}
>
`
diff --git a/src/panels/config/integrations/integration-panels/matter/dialog-matter-manage-fabrics.ts b/src/panels/config/integrations/integration-panels/matter/dialog-matter-manage-fabrics.ts
index 5f1a3ef13f0e..f10c634be9ba 100644
--- a/src/panels/config/integrations/integration-panels/matter/dialog-matter-manage-fabrics.ts
+++ b/src/panels/config/integrations/integration-panels/matter/dialog-matter-manage-fabrics.ts
@@ -1,5 +1,5 @@
import "@material/mwc-button/mwc-button";
-import { mdiClose } from "@mdi/js";
+import { mdiDelete } from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
@@ -70,7 +70,7 @@ class DialogMatterManageFabrics extends LitElement {
@click=${this._removeFabric}
slot="meta"
.fabric=${fabric}
- .path=${mdiClose}
+ .path=${mdiDelete}
>
`
)}
diff --git a/src/panels/config/integrations/integration-panels/matter/dialog-matter-open-commissioning-window.ts b/src/panels/config/integrations/integration-panels/matter/dialog-matter-open-commissioning-window.ts
index 54d74a039e31..6125b1ef97b4 100644
--- a/src/panels/config/integrations/integration-panels/matter/dialog-matter-open-commissioning-window.ts
+++ b/src/panels/config/integrations/integration-panels/matter/dialog-matter-open-commissioning-window.ts
@@ -48,6 +48,11 @@ class DialogMatterOpenCommissioningWindow extends LitElement {
>
${this._commissionParams
? html`
+
+ ${this.hass.localize(
+ "ui.panel.config.matter.open_commissioning_window.success"
+ )}
+
diff --git a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts
index dbf0ae07a0ea..a8a7ed8a8c07 100644
--- a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts
+++ b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts
@@ -19,7 +19,6 @@ import {
ConfigEntry,
getConfigEntries,
} from "../../../../../data/config_entries";
-import { computeRTL } from "../../../../../common/util/compute_rtl";
import "../../../../../components/ha-card";
import "../../../../../components/ha-fab";
import "../../../../../components/ha-icon-button";
@@ -259,7 +258,6 @@ class ZHAConfigDashboard extends LitElement {
diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts
index 78a4871dd5ef..d68fdb0b1eb5 100644
--- a/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts
+++ b/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts
@@ -1,7 +1,6 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
-import { computeRTLDirection } from "../../../../../common/util/compute_rtl";
import "../../../../../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
@@ -142,7 +141,6 @@ export class ZHADeviceEndpointDataTable extends LitElement {
.data=${this._deviceEndpoints(this.deviceEndpoints)}
.selectable=${this.selectable}
auto-height
- .dir=${computeRTLDirection(this.hass)}
.searchLabel=${this.hass.localize("ui.components.data-table.search")}
.noDataText=${this.hass.localize("ui.components.data-table.no-data")}
>
diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-neighbors.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-neighbors.ts
index 64bec7d1bcd2..0774f8b34385 100644
--- a/src/panels/config/integrations/integration-panels/zha/zha-device-neighbors.ts
+++ b/src/panels/config/integrations/integration-panels/zha/zha-device-neighbors.ts
@@ -1,7 +1,6 @@
import { html, LitElement, PropertyValues, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
-import { computeRTLDirection } from "../../../../../common/util/compute_rtl";
import "../../../../../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
@@ -128,7 +127,6 @@ class ZHADeviceNeighbors extends LitElement {
.columns=${this._columns(this.narrow)}
.data=${this._deviceNeighbors(this.device, this._devices)}
auto-height
- .dir=${computeRTLDirection(this.hass)}
.searchLabel=${this.hass.localize(
"ui.components.data-table.search"
)}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts
index e7e8a21e0ad3..16f00a7178ac 100644
--- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts
+++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts
@@ -18,7 +18,6 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
-import { computeRTL } from "../../../../../common/util/compute_rtl";
import "../../../../../components/ha-card";
import "../../../../../components/ha-expansion-panel";
import "../../../../../components/ha-fab";
@@ -472,7 +471,6 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
"ui.panel.config.zwave_js.common.add_node"
)}
extended
- ?rtl=${computeRTL(this.hass)}
@click=${this._addNodeClicked}
.disabled=${this._status !== "connected" ||
(this._network?.controller.inclusion_state !== InclusionState.Idle &&
diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts
index aa46a532a872..aca24a71acdc 100644
--- a/src/panels/config/script/ha-script-picker.ts
+++ b/src/panels/config/script/ha-script-picker.ts
@@ -26,7 +26,6 @@ import { relativeTime } from "../../../common/datetime/relative_time";
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
-import { computeRTL } from "../../../common/util/compute_rtl";
import {
DataTableColumnContainer,
RowClickedEvent,
@@ -308,7 +307,6 @@ class HaScriptPicker extends LitElement {
"ui.panel.config.script.picker.add_script"
)}
extended
- ?rtl=${computeRTL(this.hass)}
@click=${this._createNew}
>
diff --git a/src/panels/config/users/dialog-add-user.ts b/src/panels/config/users/dialog-add-user.ts
index 3e63f216ee4f..e802ecdd514a 100644
--- a/src/panels/config/users/dialog-add-user.ts
+++ b/src/panels/config/users/dialog-add-user.ts
@@ -8,7 +8,6 @@ import {
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
-import { computeRTLDirection } from "../../../common/util/compute_rtl";
import "../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-formfield";
@@ -161,7 +160,6 @@ export class DialogAddUser extends LitElement {
.label=${this.hass.localize(
"ui.panel.config.users.editor.local_only"
)}
- .dir=${computeRTLDirection(this.hass)}
>
`
: html``}
diff --git a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts
index 68204c96addc..46f4f6c372c4 100644
--- a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts
+++ b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts
@@ -27,7 +27,6 @@ import {
isEmptyFilter,
} from "../../../common/entity/entity_filter";
import { navigate } from "../../../common/navigate";
-import { computeRTL } from "../../../common/util/compute_rtl";
import {
DataTableColumnContainer,
DataTableRowData,
@@ -618,7 +617,6 @@ export class VoiceAssistantsExpose extends LitElement {
"ui.panel.config.voice_assistants.expose.add"
)}
extended
- ?rtl=${computeRTL(this.hass)}
@click=${this._addEntry}
>
diff --git a/src/panels/developer-tools/service/developer-tools-service.ts b/src/panels/developer-tools/service/developer-tools-service.ts
index d920065d5a7b..fb5f374f21d2 100644
--- a/src/panels/developer-tools/service/developer-tools-service.ts
+++ b/src/panels/developer-tools/service/developer-tools-service.ts
@@ -588,13 +588,10 @@ class HaPanelDevService extends LitElement {
}
.attributes th {
- text-align: left;
+ text-align: var(--float-start);
background-color: var(--card-background-color);
border-bottom: 1px solid var(--primary-text-color);
- }
-
- :host([rtl]) .attributes th {
- text-align: right;
+ direction: var(--direction);
}
.attributes tr {
diff --git a/src/panels/developer-tools/state/developer-tools-state.ts b/src/panels/developer-tools/state/developer-tools-state.ts
index d82d2bc2bb87..dc7f04ed6abc 100644
--- a/src/panels/developer-tools/state/developer-tools-state.ts
+++ b/src/panels/developer-tools/state/developer-tools-state.ts
@@ -16,9 +16,7 @@ import memoizeOne from "memoize-one";
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
import { storage } from "../../../common/decorators/storage";
import { fireEvent } from "../../../common/dom/fire_event";
-import { toggleAttribute } from "../../../common/dom/toggle_attribute";
import { escapeRegExp } from "../../../common/string/escape_regexp";
-import { computeRTL } from "../../../common/util/compute_rtl";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-alert";
@@ -69,8 +67,6 @@ class HaPanelDevState extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false;
- @property({ type: Boolean, reflect: true }) public rtl = false;
-
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
private _filteredEntities = memoizeOne(
@@ -325,14 +321,6 @@ class HaPanelDevState extends LitElement {
`;
}
- protected updated(changedProps) {
- super.updated(changedProps);
- const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
- if (!oldHass || oldHass.locale !== this.hass.locale) {
- toggleAttribute(this, "rtl", computeRTL(this.hass));
- }
- }
-
private _copyEntity(ev) {
ev.preventDefault();
const entity = (ev.currentTarget! as any).entity;
@@ -626,7 +614,8 @@ class HaPanelDevState extends LitElement {
.entities th {
padding: 0 8px;
- text-align: left;
+ text-align: var(--float-start);
+ direction: var(--direction);
}
.filters th {
@@ -652,15 +641,6 @@ class HaPanelDevState extends LitElement {
bottom: -8px;
}
- :host([rtl]) .entities th {
- text-align: right;
- direction: rtl;
- }
-
- :host([rtl]) .filters {
- direction: rtl;
- }
-
.entities tr {
vertical-align: top;
direction: ltr;
diff --git a/src/panels/developer-tools/statistics/developer-tools-statistics.ts b/src/panels/developer-tools/statistics/developer-tools-statistics.ts
index f85744ec45e5..0cb139971695 100644
--- a/src/panels/developer-tools/statistics/developer-tools-statistics.ts
+++ b/src/panels/developer-tools/statistics/developer-tools-statistics.ts
@@ -1,7 +1,7 @@
import "@material/mwc-button/mwc-button";
import { mdiSlopeUphill } from "@mdi/js";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
-import { css, CSSResultGroup, html, LitElement } from "lit";
+import { CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -26,7 +26,6 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed";
-import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { documentationUrl } from "../../../util/documentation-url";
const FIX_ISSUES_ORDER = {
@@ -183,7 +182,6 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
id="statistic_id"
clickable
@row-click=${this._rowClicked}
- .dir=${computeRTLDirection(this.hass)}
>
`;
}
@@ -411,45 +409,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
};
static get styles(): CSSResultGroup {
- return [
- haStyle,
- css`
- .content {
- padding: 16px;
- padding: max(16px, env(safe-area-inset-top))
- max(16px, env(safe-area-inset-right))
- max(16px, env(safe-area-inset-bottom))
- max(16px, env(safe-area-inset-left));
- }
-
- th {
- padding: 0 8px;
- text-align: left;
- }
-
- :host([rtl]) th {
- text-align: right;
- }
-
- tr {
- vertical-align: top;
- direction: ltr;
- }
-
- tr:nth-child(odd) {
- background-color: var(--table-row-background-color, #fff);
- }
-
- tr:nth-child(even) {
- background-color: var(--table-row-alternative-background-color, #eee);
- }
- td {
- padding: 4px;
- min-width: 200px;
- word-break: break-word;
- }
- `,
- ];
+ return haStyle;
}
}
diff --git a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts
index abc264adce6d..6ffa9790da95 100644
--- a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts
+++ b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts
@@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button";
-import "@material/mwc-list/mwc-list-item";
import formatISO9075 from "date-fns/formatISO9075";
import {
css,
@@ -21,6 +20,7 @@ import "../../../components/ha-selector/ha-selector-datetime";
import "../../../components/ha-selector/ha-selector-number";
import "../../../components/ha-svg-icon";
import "../../../components/ha-icon-next";
+import "../../../components/ha-list-item";
import {
adjustStatisticsSum,
fetchStatistics,
@@ -151,7 +151,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
const stat = data[i];
const growth = Math.round(stat.change! * 100) / 100;
rows.push(html`
-
-
+
`);
}
stats = html`${rows}`;
@@ -413,7 +413,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
ha-selector-number {
margin-bottom: 20px;
}
- mwc-list-item {
+ ha-list-item {
margin: 0 -24px;
--mdc-list-side-padding: 24px;
}
diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts
index fbed2990a971..52f640e2c4a2 100644
--- a/src/panels/history/ha-panel-history.ts
+++ b/src/panels/history/ha-panel-history.ts
@@ -1,4 +1,4 @@
-import { mdiDownload, mdiFilterRemove, mdiRefresh } from "@mdi/js";
+import { mdiDownload, mdiFilterRemove } from "@mdi/js";
import { differenceInHours } from "date-fns/esm";
import {
HassServiceTarget,
@@ -6,6 +6,7 @@ import {
} from "home-assistant-js-websocket/dist/types";
import { LitElement, PropertyValues, css, html } from "lit";
import { property, query, state } from "lit/decorators";
+import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { storage } from "../../common/decorators/storage";
import { navigate } from "../../common/navigate";
@@ -15,7 +16,6 @@ import {
extractSearchParamsObject,
removeSearchParam,
} from "../../common/url/search-params";
-import { computeRTL } from "../../common/util/compute_rtl";
import { MIN_TIME_BETWEEN_UPDATES } from "../../components/chart/ha-chart-base";
import "../../components/chart/state-history-charts";
import type { StateHistoryCharts } from "../../components/chart/state-history-charts";
@@ -45,8 +45,8 @@ import {
HistoryStates,
EntityHistoryState,
LineChartUnit,
- LineChartEntity,
computeGroupKey,
+ LineChartState,
} from "../../data/history";
import { fetchStatistics, Statistics } from "../../data/recorder";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
@@ -54,6 +54,7 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { fileDownload } from "../../util/file_download";
+import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
class HaPanelHistory extends SubscribeMixin(LitElement) {
@property({ attribute: false }) hass!: HomeAssistant;
@@ -71,7 +72,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
state: true,
subscribe: false,
})
- private _targetPickerValue?: HassServiceTarget;
+ private _targetPickerValue: HassServiceTarget = {};
@state() private _isLoading = false;
@@ -138,6 +139,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
}
protected render() {
+ const entitiesSelected = this._getEntityIds().length > 0;
return html`
${this._showBack
@@ -155,7 +157,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
>
`}
${this.hass.localize("panel.history")}
- ${this._targetPickerValue
+ ${entitiesSelected
? html`
`
: ""}
-
@@ -203,7 +198,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
? html`
`
- : !this._targetPickerValue
+ : !entitiesSelected
? html`
${this.hass.localize("ui.panel.history.start_search")}
`
@@ -227,50 +222,83 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
): HistoryResult {
const result: HistoryResult = { ...historyResult, line: [] };
- const keys = new Set(
- historyResult.line
- .map((i) => computeGroupKey(i.unit, i.device_class, true))
- .concat(
- ltsResult.line.map((i) =>
- computeGroupKey(i.unit, i.device_class, true)
- )
- )
- );
- keys.forEach((key) => {
- const historyItem = historyResult.line.find(
- (i) => computeGroupKey(i.unit, i.device_class, true) === key
- );
- const ltsItem = ltsResult.line.find(
- (i) => computeGroupKey(i.unit, i.device_class, true) === key
- );
- if (historyItem && ltsItem) {
- const newLineItem: LineChartUnit = { ...historyItem, data: [] };
- const entities = new Set(
- historyItem.data
- .map((d) => d.entity_id)
- .concat(ltsItem.data.map((d) => d.entity_id))
- );
- entities.forEach((entity) => {
- const historyDataItem = historyItem.data.find(
- (d) => d.entity_id === entity
- );
- const ltsDataItem = ltsItem.data.find((d) => d.entity_id === entity);
- if (historyDataItem && ltsDataItem) {
- const newDataItem: LineChartEntity = {
- ...historyDataItem,
- statistics: ltsDataItem.statistics,
- };
- newLineItem.data.push(newDataItem);
- } else {
- newLineItem.data.push(historyDataItem || ltsDataItem!);
- }
- });
- result.line.push(newLineItem);
+ const lookup: Record<
+ string,
+ { historyItem?: LineChartUnit; ltsItem?: LineChartUnit }
+ > = {};
+
+ for (const item of historyResult.line) {
+ const key = computeGroupKey(item.unit, item.device_class, true);
+ if (key) {
+ lookup[key] = {
+ historyItem: item,
+ };
+ }
+ }
+
+ for (const item of ltsResult.line) {
+ const key = computeGroupKey(item.unit, item.device_class, true);
+ if (!key) {
+ continue;
+ }
+ if (key in lookup) {
+ lookup[key].ltsItem = item;
} else {
+ lookup[key] = { ltsItem: item };
+ }
+ }
+
+ for (const { historyItem, ltsItem } of Object.values(lookup)) {
+ if (!historyItem || !ltsItem) {
// Only one result has data for this item, so just push it directly instead of merging.
result.line.push(historyItem || ltsItem!);
+ continue;
}
- });
+
+ const newLineItem: LineChartUnit = { ...historyItem, data: [] };
+ const entities = new Set([
+ ...historyItem.data.map((d) => d.entity_id),
+ ...ltsItem.data.map((d) => d.entity_id),
+ ]);
+
+ for (const entity of entities) {
+ const historyDataItem = historyItem.data.find(
+ (d) => d.entity_id === entity
+ );
+ const ltsDataItem = ltsItem.data.find((d) => d.entity_id === entity);
+
+ if (!historyDataItem || !ltsDataItem) {
+ newLineItem.data.push(historyDataItem || ltsDataItem!);
+ continue;
+ }
+
+ // Remove statistics that overlap with states
+ const oldestState =
+ historyDataItem.states[0]?.last_changed ||
+ // If no state, fall back to the max last changed of the last statistics (so approve all)
+ ltsDataItem.statistics![ltsDataItem.statistics!.length - 1]
+ .last_changed + 1;
+
+ const statistics: LineChartState[] = [];
+ for (const s of ltsDataItem.statistics!) {
+ if (s.last_changed >= oldestState) {
+ break;
+ }
+ statistics.push(s);
+ }
+
+ newLineItem.data.push(
+ statistics.length === 0
+ ? // All statistics overlapped with states, so just push the states
+ historyDataItem
+ : {
+ ...historyDataItem,
+ statistics,
+ }
+ );
+ }
+ result.line.push(newLineItem);
+ }
return result;
}
@@ -342,114 +370,90 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
protected updated(changedProps: PropertyValues) {
if (
- this._targetPickerValue &&
- (changedProps.has("_startDate") ||
- changedProps.has("_endDate") ||
- changedProps.has("_targetPickerValue") ||
- (!this._stateHistory &&
- (changedProps.has("_deviceEntityLookup") ||
- changedProps.has("_areaEntityLookup") ||
- changedProps.has("_areaDeviceLookup"))))
+ changedProps.has("_startDate") ||
+ changedProps.has("_endDate") ||
+ changedProps.has("_targetPickerValue") ||
+ (!this._stateHistory &&
+ (changedProps.has("_deviceEntityLookup") ||
+ changedProps.has("_areaEntityLookup") ||
+ changedProps.has("_areaDeviceLookup")))
) {
this._getHistory();
this._getStats();
}
-
- if (!changedProps.has("hass") && !changedProps.has("_entities")) {
- return;
- }
-
- const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
- if (!oldHass || oldHass.language !== this.hass.language) {
- this.rtl = computeRTL(this.hass);
- }
}
private _removeAll() {
- this._targetPickerValue = undefined;
+ this._targetPickerValue = {};
this._updatePath();
}
private async _getStats() {
- const entityIds = this._getEntityIds();
- if (!entityIds) {
+ const statisticIds = this._getEntityIds();
+
+ if (statisticIds.length === 0) {
+ this._statisticsHistory = undefined;
return;
}
- this._getStatistics(entityIds);
- }
- private async _getStatistics(statisticIds: string[]): Promise {
- try {
- const statistics = await fetchStatistics(
- this.hass!,
- this._startDate,
- this._endDate,
- statisticIds,
- "hour",
- undefined,
- ["mean", "state"]
- );
+ const statistics = await fetchStatistics(
+ this.hass!,
+ this._startDate,
+ this._endDate,
+ statisticIds,
+ "hour",
+ undefined,
+ ["mean", "state"]
+ );
- // Maintain the statistic id ordering
- const orderedStatistics: Statistics = {};
- statisticIds.forEach((id) => {
- if (id in statistics) {
- orderedStatistics[id] = statistics[id];
- }
- });
+ // Maintain the statistic id ordering
+ const orderedStatistics: Statistics = {};
+ statisticIds.forEach((id) => {
+ if (id in statistics) {
+ orderedStatistics[id] = statistics[id];
+ }
+ });
- // Convert statistics to HistoryResult format
- const statsHistoryStates: HistoryStates = {};
- Object.entries(orderedStatistics).forEach(([key, value]) => {
- const entityHistoryStates: EntityHistoryState[] = value.map((e) => ({
- s: e.mean != null ? e.mean.toString() : e.state!.toString(),
- lc: e.start / 1000,
- a: {},
- lu: e.start / 1000,
- }));
- statsHistoryStates[key] = entityHistoryStates;
- });
+ // Convert statistics to HistoryResult format
+ const statsHistoryStates: HistoryStates = {};
+ Object.entries(orderedStatistics).forEach(([key, value]) => {
+ const entityHistoryStates: EntityHistoryState[] = value.map((e) => ({
+ s: e.mean != null ? e.mean.toString() : e.state!.toString(),
+ lc: e.start / 1000,
+ a: {},
+ lu: e.start / 1000,
+ }));
+ statsHistoryStates[key] = entityHistoryStates;
+ });
- const { numeric_device_classes: sensorNumericDeviceClasses } =
- await getSensorNumericDeviceClasses(this.hass);
+ const { numeric_device_classes: sensorNumericDeviceClasses } =
+ await getSensorNumericDeviceClasses(this.hass);
- this._statisticsHistory = computeHistory(
- this.hass,
- statsHistoryStates,
- this.hass.localize,
- sensorNumericDeviceClasses,
- true
- );
- // remap states array to statistics array
- (this._statisticsHistory?.line || []).forEach((item) => {
- item.data.forEach((data) => {
- data.statistics = data.states;
- data.states = [];
- });
+ this._statisticsHistory = computeHistory(
+ this.hass,
+ statsHistoryStates,
+ this.hass.localize,
+ sensorNumericDeviceClasses,
+ true
+ );
+ // remap states array to statistics array
+ (this._statisticsHistory?.line || []).forEach((item) => {
+ item.data.forEach((data) => {
+ data.statistics = data.states;
+ data.states = [];
});
- } catch (err) {
- this._statisticsHistory = undefined;
- }
+ });
}
private async _getHistory() {
- if (!this._targetPickerValue) {
- return;
- }
- this._isLoading = true;
const entityIds = this._getEntityIds();
- if (entityIds === undefined) {
- this._isLoading = false;
+ if (entityIds.length === 0) {
this._stateHistory = undefined;
return;
}
- if (entityIds.length === 0) {
- this._isLoading = false;
- this._stateHistory = { line: [], timeline: [] };
- return;
- }
+ this._isLoading = true;
if (this._subscribed) {
this._unsubscribeHistory();
@@ -512,84 +516,100 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
}
}
- private _getEntityIds(): string[] | undefined {
- if (
- !this._targetPickerValue ||
- this._deviceEntityLookup === undefined ||
- this._areaEntityLookup === undefined ||
- this._areaDeviceLookup === undefined
- ) {
- return undefined;
- }
+ private _getEntityIds(): string[] {
+ return this.__getEntityIds(
+ this._targetPickerValue,
+ this._deviceEntityLookup,
+ this._areaEntityLookup,
+ this._areaDeviceLookup
+ );
+ }
- const entityIds = new Set();
- let {
- area_id: searchingAreaId,
- device_id: searchingDeviceId,
- entity_id: searchingEntityId,
- } = this._targetPickerValue;
-
- if (searchingAreaId) {
- searchingAreaId = ensureArray(searchingAreaId);
- for (const singleSearchingAreaId of searchingAreaId) {
- const foundEntities = this._areaEntityLookup[singleSearchingAreaId];
- if (foundEntities?.length) {
- for (const foundEntity of foundEntities) {
- if (foundEntity.entity_category === null) {
- entityIds.add(foundEntity.entity_id);
+ private __getEntityIds = memoizeOne(
+ (
+ targetPickerValue: HassServiceTarget,
+ deviceEntityLookup: DeviceEntityLookup | undefined,
+ areaEntityLookup: AreaEntityLookup | undefined,
+ areaDeviceLookup: AreaDeviceLookup | undefined
+ ): string[] => {
+ if (
+ !targetPickerValue ||
+ deviceEntityLookup === undefined ||
+ areaEntityLookup === undefined ||
+ areaDeviceLookup === undefined
+ ) {
+ return [];
+ }
+
+ const entityIds = new Set();
+ let {
+ area_id: searchingAreaId,
+ device_id: searchingDeviceId,
+ entity_id: searchingEntityId,
+ } = targetPickerValue;
+
+ if (searchingAreaId) {
+ searchingAreaId = ensureArray(searchingAreaId);
+ for (const singleSearchingAreaId of searchingAreaId) {
+ const foundEntities = areaEntityLookup[singleSearchingAreaId];
+ if (foundEntities?.length) {
+ for (const foundEntity of foundEntities) {
+ if (foundEntity.entity_category === null) {
+ entityIds.add(foundEntity.entity_id);
+ }
}
}
- }
-
- const foundDevices = this._areaDeviceLookup[singleSearchingAreaId];
- if (!foundDevices?.length) {
- continue;
- }
- for (const foundDevice of foundDevices) {
- const foundDeviceEntities = this._deviceEntityLookup[foundDevice.id];
- if (!foundDeviceEntities?.length) {
+ const foundDevices = areaDeviceLookup[singleSearchingAreaId];
+ if (!foundDevices?.length) {
continue;
}
- for (const foundDeviceEntity of foundDeviceEntities) {
- if (
- (!foundDeviceEntity.area_id ||
- foundDeviceEntity.area_id === singleSearchingAreaId) &&
- foundDeviceEntity.entity_category === null
- ) {
- entityIds.add(foundDeviceEntity.entity_id);
+ for (const foundDevice of foundDevices) {
+ const foundDeviceEntities = deviceEntityLookup[foundDevice.id];
+ if (!foundDeviceEntities?.length) {
+ continue;
+ }
+
+ for (const foundDeviceEntity of foundDeviceEntities) {
+ if (
+ (!foundDeviceEntity.area_id ||
+ foundDeviceEntity.area_id === singleSearchingAreaId) &&
+ foundDeviceEntity.entity_category === null
+ ) {
+ entityIds.add(foundDeviceEntity.entity_id);
+ }
}
}
}
}
- }
- if (searchingDeviceId) {
- searchingDeviceId = ensureArray(searchingDeviceId);
- for (const singleSearchingDeviceId of searchingDeviceId) {
- const foundEntities = this._deviceEntityLookup[singleSearchingDeviceId];
- if (!foundEntities?.length) {
- continue;
- }
+ if (searchingDeviceId) {
+ searchingDeviceId = ensureArray(searchingDeviceId);
+ for (const singleSearchingDeviceId of searchingDeviceId) {
+ const foundEntities = deviceEntityLookup[singleSearchingDeviceId];
+ if (!foundEntities?.length) {
+ continue;
+ }
- for (const foundEntity of foundEntities) {
- if (foundEntity.entity_category === null) {
- entityIds.add(foundEntity.entity_id);
+ for (const foundEntity of foundEntities) {
+ if (foundEntity.entity_category === null) {
+ entityIds.add(foundEntity.entity_id);
+ }
}
}
}
- }
- if (searchingEntityId) {
- searchingEntityId = ensureArray(searchingEntityId);
- for (const singleSearchingEntityId of searchingEntityId) {
- entityIds.add(singleSearchingEntityId);
+ if (searchingEntityId) {
+ searchingEntityId = ensureArray(searchingEntityId);
+ for (const singleSearchingEntityId of searchingEntityId) {
+ entityIds.add(singleSearchingEntityId);
+ }
}
- }
- return [...entityIds];
- }
+ return [...entityIds];
+ }
+ );
private _dateRangeChanged(ev) {
this._startDate = ev.detail.startDate;
@@ -611,20 +631,18 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
private _updatePath() {
const params: Record = {};
- if (this._targetPickerValue) {
- if (this._targetPickerValue.entity_id) {
- params.entity_id = ensureArray(this._targetPickerValue.entity_id).join(
- ","
- );
- }
- if (this._targetPickerValue.area_id) {
- params.area_id = ensureArray(this._targetPickerValue.area_id).join(",");
- }
- if (this._targetPickerValue.device_id) {
- params.device_id = ensureArray(this._targetPickerValue.device_id).join(
- ","
- );
- }
+ if (this._targetPickerValue.entity_id) {
+ params.entity_id = ensureArray(this._targetPickerValue.entity_id).join(
+ ","
+ );
+ }
+ if (this._targetPickerValue.area_id) {
+ params.area_id = ensureArray(this._targetPickerValue.area_id).join(",");
+ }
+ if (this._targetPickerValue.device_id) {
+ params.device_id = ensureArray(this._targetPickerValue.device_id).join(
+ ","
+ );
}
if (this._startDate) {
@@ -639,23 +657,35 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
}
private _downloadHistory() {
+ const entities = this._getEntityIds();
+ if (entities.length === 0 || !this._mungedStateHistory) {
+ showAlertDialog(this, {
+ title: this.hass.localize("ui.panel.history.download_data_error"),
+ text: this.hass.localize("ui.panel.history.error_no_data"),
+ warning: true,
+ });
+ return;
+ }
+
const csv: string[] = ["entity_id,state,last_changed\n"];
const formatDate = (number) => new Date(number).toISOString();
- for (const line of this._mungedStateHistory!.line) {
+ for (const line of this._mungedStateHistory.line) {
for (const entity of line.data) {
const entityId = entity.entity_id;
- for (const data of [entity.states, entity.statistics]) {
- if (!data) {
- continue;
- }
- for (const s of data) {
+
+ if (entity.statistics) {
+ for (const s of entity.statistics) {
csv.push(`${entityId},${s.state},${formatDate(s.last_changed)}\n`);
}
}
+
+ for (const s of entity.states) {
+ csv.push(`${entityId},${s.state},${formatDate(s.last_changed)}\n`);
+ }
}
}
- for (const timeline of this._mungedStateHistory!.timeline) {
+ for (const timeline of this._mungedStateHistory.timeline) {
const entityId = timeline.entity_id;
for (const s of timeline.data) {
csv.push(`${entityId},${s.state},${formatDate(s.last_changed)}\n`);
@@ -710,6 +740,7 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
ha-date-range-picker {
margin-right: 0;
margin-inline-end: 0;
+ margin-inline-start: initial;
width: 100%;
}
}
diff --git a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts
index b24df99a3264..f7512fc981ef 100644
--- a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts
+++ b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts
@@ -215,6 +215,8 @@ class HuiEnergyCarbonGaugeCard
ha-svg-icon {
position: absolute;
right: 4px;
+ inset-inline-end: 4px;
+ inset-inline-start: initial;
top: 4px;
color: var(--secondary-text-color);
}
diff --git a/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts
index 1acb16fef7bc..558762ca15b8 100644
--- a/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts
+++ b/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts
@@ -192,6 +192,8 @@ class HuiEnergyGridGaugeCard
ha-svg-icon {
position: absolute;
right: 4px;
+ inset-inline-end: 4px;
+ inset-inline-start: initial;
top: 4px;
color: var(--secondary-text-color);
}
diff --git a/src/panels/lovelace/cards/energy/hui-energy-self-sufficiency-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-self-sufficiency-gauge-card.ts
index 1f87f7167ff8..e39450c4ff3c 100644
--- a/src/panels/lovelace/cards/energy/hui-energy-self-sufficiency-gauge-card.ts
+++ b/src/panels/lovelace/cards/energy/hui-energy-self-sufficiency-gauge-card.ts
@@ -255,6 +255,8 @@ class HuiEnergySelfSufficiencyGaugeCard
ha-svg-icon {
position: absolute;
right: 4px;
+ inset-inline-end: 4px;
+ inset-inline-start: initial;
top: 4px;
color: var(--secondary-text-color);
}
diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts
index fb70fbcca4b2..78ffa6c75078 100644
--- a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts
+++ b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts
@@ -194,6 +194,8 @@ class HuiEnergySolarGaugeCard
ha-svg-icon {
position: absolute;
right: 4px;
+ inset-inline-end: 4px;
+ inset-inline-start: initial;
top: 4px;
color: var(--secondary-text-color);
}
diff --git a/src/panels/lovelace/cards/hui-button-card.ts b/src/panels/lovelace/cards/hui-button-card.ts
index 703ad164cfff..b7eddb962c15 100644
--- a/src/panels/lovelace/cards/hui-button-card.ts
+++ b/src/panels/lovelace/cards/hui-button-card.ts
@@ -187,8 +187,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
return html`
144) {
- // if showing > 6 days in the history trail, show the full
- // date and time
- p.tooltip = formatDateTime(t, this.hass.locale, this.hass.config);
- } else if (isToday(t)) {
- p.tooltip = formatTimeWithSeconds(
- t,
- this.hass.locale,
- this.hass.config
- );
- } else {
- p.tooltip = formatTimeWeekday(
- t,
- this.hass.locale,
- this.hass.config
- );
- }
+ p.timestamp = new Date(entityState.lu * 1000);
points.push(p);
}
+
+ const entityConfig = this._configEntities?.find(
+ (e) => e.entity === entityId
+ );
+ const name =
+ entityConfig?.name ??
+ (entityId in this.hass.states
+ ? computeStateName(this.hass.states[entityId])
+ : entityId);
+
paths.push({
points,
+ name,
+ fullDatetime: (config.hours_to_show ?? DEFAULT_HOURS_TO_SHOW) > 144,
color: this._getColor(entityId),
gradualOpacity: 0.8,
});
diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts
index 98faa89eaf1a..b1c279091a53 100644
--- a/src/panels/lovelace/cards/hui-tile-card.ts
+++ b/src/panels/lovelace/cards/hui-tile-card.ts
@@ -524,6 +524,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
position: absolute;
top: -3px;
right: -3px;
+ inset-inline-end: -3px;
+ inset-inline-start: initial;
}
.icon-container:not([role="button"]) {
pointer-events: none;
diff --git a/src/panels/lovelace/cards/hui-weather-forecast-card.ts b/src/panels/lovelace/cards/hui-weather-forecast-card.ts
index 781f02bd42fd..3d0d2ac047f2 100644
--- a/src/panels/lovelace/cards/hui-weather-forecast-card.ts
+++ b/src/panels/lovelace/cards/hui-weather-forecast-card.ts
@@ -612,11 +612,6 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
text-align: center;
}
- /* ============= RTL ============= */
- :host([rtl]) .temp-attribute {
- text-align: left;
- }
-
/* ============= NARROW ============= */
:host([narrow]) .icon-image {
diff --git a/src/panels/lovelace/components/hui-generic-entity-row.ts b/src/panels/lovelace/components/hui-generic-entity-row.ts
index f58677657edd..262459b2bee8 100644
--- a/src/panels/lovelace/components/hui-generic-entity-row.ts
+++ b/src/panels/lovelace/components/hui-generic-entity-row.ts
@@ -13,7 +13,6 @@ import { DOMAINS_INPUT_ROW } from "../../../common/const";
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
-import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/entity/state-badge";
import "../../../components/ha-relative-time";
import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
@@ -186,9 +185,6 @@ export class HuiGenericEntityRow extends LitElement {
"no-secondary",
!this.secondaryText && !this.config?.secondary_info
);
- if (changedProps.has("hass")) {
- toggleAttribute(this, "rtl", computeRTL(this.hass!));
- }
}
private _handleAction(ev: ActionHandlerEvent) {
@@ -233,22 +229,11 @@ export class HuiGenericEntityRow extends LitElement {
state-badge {
flex: 0 0 40px;
}
- :host([rtl]) .flex {
- margin-left: 0;
- margin-right: 16px;
- }
- :host([rtl]) .flex ::slotted(*) {
- margin-left: 0;
- margin-right: 8px;
- }
.pointer {
cursor: pointer;
}
.state {
- text-align: right;
- }
- .state.rtl {
- text-align: left;
+ text-align: var(--float-end);
}
.value {
direction: ltr;
diff --git a/src/panels/lovelace/editor/card-editor/hui-card-preview.ts b/src/panels/lovelace/editor/card-editor/hui-card-preview.ts
index a1c95829009f..72a652834afc 100644
--- a/src/panels/lovelace/editor/card-editor/hui-card-preview.ts
+++ b/src/panels/lovelace/editor/card-editor/hui-card-preview.ts
@@ -1,6 +1,5 @@
import { PropertyValues, ReactiveElement } from "lit";
import { property } from "lit/decorators";
-import { computeRTL } from "../../../../common/util/compute_rtl";
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import { HomeAssistant } from "../../../../types";
import { createCardElement } from "../../create-element/create-card-element";
@@ -70,13 +69,6 @@ export class HuiCardPreview extends ReactiveElement {
}
if (changedProperties.has("hass")) {
- const oldHass = changedProperties.get("hass") as
- | HomeAssistant
- | undefined;
- if (!oldHass || oldHass.language !== this.hass!.language) {
- this.style.direction = computeRTL(this.hass!) ? "rtl" : "ltr";
- }
-
if (this._element) {
this._element.hass = this.hass;
}
diff --git a/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts b/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts
index 997a4ec22dac..51f42252c1f0 100644
--- a/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts
+++ b/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts
@@ -3,7 +3,6 @@ import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
-import { computeRTLDirection } from "../../../../common/util/compute_rtl";
import "../../../../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
@@ -33,7 +32,6 @@ export class HuiEntityPickerTable extends LitElement {
.id=${"entity_id"}
.columns=${this._columns(this.narrow!)}
.data=${this.entities}
- .dir=${computeRTLDirection(this.hass)}
.searchLabel=${this.hass.localize(
"ui.panel.lovelace.unused_entities.search"
)}
diff --git a/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts
index 8e6eb4bbecc2..106b0b97ec40 100644
--- a/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts
@@ -18,7 +18,6 @@ import {
} from "superstruct";
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
import { customType } from "../../../../common/structs/is-custom-type";
-import { computeRTLDirection } from "../../../../common/util/compute_rtl";
import "../../../../components/ha-card";
import "../../../../components/ha-formfield";
import "../../../../components/ha-icon";
@@ -265,7 +264,6 @@ export class HuiEntitiesCardEditor
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.entities.show_header_toggle"
)}
- .dir=${computeRTLDirection(this.hass)}
>
@@ -171,20 +169,13 @@ export class HuiUnusedEntities extends LitElement {
}
.fab {
position: sticky;
- float: right;
+ float: var(--float-end);
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));
+ inset-inline-start: initial;
z-index: 1;
}
- .fab.rtl {
- right: initial;
- left: 0;
- bottom: 0;
- padding-right: 16px;
- padding-left: calc(16px + env(safe-area-inset-left));
- padding-inline-end: 16px;
- padding-inline-start: calc(16px + env(safe-area-inset-left));
- }
ha-fab {
position: relative;
bottom: calc(-80px - env(safe-area-inset-bottom));
diff --git a/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts
index 65c0ef1e2774..f6888a5d02af 100644
--- a/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts
+++ b/src/panels/lovelace/entity-rows/hui-input-number-entity-row.ts
@@ -7,7 +7,6 @@ import {
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
-import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-slider";
import "../../../components/ha-textfield";
@@ -86,7 +85,6 @@ class HuiInputNumberEntityRow extends LitElement implements LovelaceRow {
;
+ private _loading = false;
+
public connectedCallback(): void {
super.connectedCallback();
if (
@@ -162,7 +164,7 @@ export class LovelacePanel extends LitElement {
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
- if (!this.lovelace && this._panelState !== "error") {
+ if (!this.lovelace && this._panelState !== "error" && !this._loading) {
this._fetchConfig(false);
}
}
@@ -233,6 +235,8 @@ export class LovelacePanel extends LitElement {
}
private async _fetchConfig(forceDiskRefresh: boolean) {
+ this._loading = true;
+
let conf: LovelaceConfig;
let rawConf: LovelaceRawConfig | undefined;
let confMode: Lovelace["mode"] = this.panel!.config.mode;
@@ -301,6 +305,7 @@ export class LovelacePanel extends LitElement {
rawConf = DEFAULT_CONFIG;
confMode = "generated";
} finally {
+ this._loading = false;
// Ignore updates for another 2 seconds.
if (this.lovelace && this.lovelace.mode === "yaml") {
setTimeout(() => {
diff --git a/src/panels/lovelace/views/hui-masonry-view.ts b/src/panels/lovelace/views/hui-masonry-view.ts
index 16f3d60882f6..bcc87214e3c6 100644
--- a/src/panels/lovelace/views/hui-masonry-view.ts
+++ b/src/panels/lovelace/views/hui-masonry-view.ts
@@ -8,9 +8,7 @@ import {
TemplateResult,
} from "lit";
import { property, state } from "lit/decorators";
-import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event";
-import { computeRTL } from "../../../common/util/compute_rtl";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/entity/ha-state-label-badge";
import "../../../components/ha-svg-icon";
@@ -97,9 +95,6 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
)}
extended
@click=${this._addCard}
- class=${classMap({
- rtl: computeRTL(this.hass!),
- })}
>
@@ -338,14 +333,11 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
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));
+ inset-inline-start: initial;
z-index: 1;
}
- ha-fab.rtl {
- right: auto;
- left: calc(16px + env(safe-area-inset-left));
- }
-
@media (max-width: 500px) {
.column > * {
margin-left: 0;
diff --git a/src/panels/lovelace/views/hui-panel-view.ts b/src/panels/lovelace/views/hui-panel-view.ts
index 2d36ba5178a6..aae451deaa5e 100644
--- a/src/panels/lovelace/views/hui-panel-view.ts
+++ b/src/panels/lovelace/views/hui-panel-view.ts
@@ -137,12 +137,9 @@ export class PanelView extends LitElement implements LovelaceViewElement {
right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
- }
-
- ha-fab.rtl {
- float: left;
- right: auto;
- left: calc(16px + env(safe-area-inset-left));
+ float: var(--float-end);
+ inset-inline-end: calc(16px + env(safe-area-inset-right));
+ inset-inline-start: initial;
}
`;
}
diff --git a/src/panels/lovelace/views/hui-sidebar-view.ts b/src/panels/lovelace/views/hui-sidebar-view.ts
index f6085637da04..1e8442880b28 100644
--- a/src/panels/lovelace/views/hui-sidebar-view.ts
+++ b/src/panels/lovelace/views/hui-sidebar-view.ts
@@ -8,9 +8,7 @@ import {
html,
} from "lit";
import { property, state } from "lit/decorators";
-import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event";
-import { computeRTL } from "../../../common/util/compute_rtl";
import type { LovelaceViewElement } from "../../../data/lovelace";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
@@ -100,9 +98,6 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
)}
extended
@click=${this._addCard}
- class=${classMap({
- rtl: computeRTL(this.hass!),
- })}
>
@@ -241,13 +236,10 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
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));
+ inset-inline-start: initial;
z-index: 1;
}
-
- ha-fab.rtl {
- right: auto;
- left: calc(16px + env(safe-area-inset-left));
- }
`;
}
}
diff --git a/src/panels/todo/ha-panel-todo.ts b/src/panels/todo/ha-panel-todo.ts
index 03a3e2b1fdb6..b1dcee7fb4de 100644
--- a/src/panels/todo/ha-panel-todo.ts
+++ b/src/panels/todo/ha-panel-todo.ts
@@ -430,6 +430,8 @@ class PanelTodo extends LitElement {
position: fixed;
right: 16px;
bottom: 16px;
+ inset-inline-end: 16px;
+ inset-inline-start: initial;
}
`,
];
diff --git a/src/resources/ha-sidebar-edit-style.ts b/src/resources/ha-sidebar-edit-style.ts
index 50bf37c75616..91d70bfa2b61 100644
--- a/src/resources/ha-sidebar-edit-style.ts
+++ b/src/resources/ha-sidebar-edit-style.ts
@@ -62,22 +62,16 @@ export const sidebarEditStyle = css`
position: absolute;
top: 0;
right: 4px;
+ inset-inline-end: 4px;
+ inset-inline-start: initial;
--mdc-icon-button-size: 40px;
}
- :host([rtl]) .show-panel {
- right: initial;
- left: 4px;
- }
-
.hide-panel {
top: 4px;
right: 8px;
- }
-
- :host([rtl]) .hide-panel {
- right: initial;
- left: 8px;
+ inset-inline-end: 8px;
+ inset-inline-start: initial;
}
:host([expanded]) .hide-panel {
diff --git a/src/state-summary/state-card-alert.ts b/src/state-summary/state-card-alert.ts
index e6ee8d3e05c3..fb85350993ab 100755
--- a/src/state-summary/state-card-alert.ts
+++ b/src/state-summary/state-card-alert.ts
@@ -2,7 +2,6 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { stateActive } from "../common/entity/state_active";
-import { computeRTL } from "../common/util/compute_rtl";
import "../components/entity/ha-entity-toggle";
import "../components/entity/state-info";
import { haStyle } from "../resources/styles";
@@ -16,9 +15,6 @@ class StateCardAlert extends LitElement {
@property({ type: Boolean }) public inDialog = false;
- // property used only in CSS
- @property({ type: Boolean, reflect: true }) public rtl = false;
-
protected render(): TemplateResult {
return html`
@@ -40,18 +36,6 @@ class StateCardAlert extends LitElement {
`;
}
- protected updated(changedProps) {
- super.updated(changedProps);
- if (!changedProps.has("hass")) {
- return;
- }
-
- const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
- if (!oldHass || oldHass.language !== this.hass.language) {
- this.rtl = computeRTL(this.hass);
- }
- }
-
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -69,7 +53,6 @@ class StateCardAlert extends LitElement {
overflow-wrap: break-word;
display: flex;
align-items: center;
- direction: ltr;
}
ha-entity-toggle {
margin: -4px -16px -4px 0;
diff --git a/src/state-summary/state-card-display.ts b/src/state-summary/state-card-display.ts
index 7263ab30bdd0..fe232520bf6b 100755
--- a/src/state-summary/state-card-display.ts
+++ b/src/state-summary/state-card-display.ts
@@ -3,7 +3,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { computeDomain } from "../common/entity/compute_domain";
-import { computeRTL } from "../common/util/compute_rtl";
import "../components/entity/state-info";
import { isUnavailableState } from "../data/entity";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../data/sensor";
@@ -53,18 +52,6 @@ class StateCardDisplay extends LitElement {
`;
}
- protected updated(changedProps) {
- super.updated(changedProps);
- if (!changedProps.has("hass")) {
- return;
- }
-
- const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
- if (!oldHass || oldHass.language !== this.hass.language) {
- this.rtl = computeRTL(this.hass);
- }
- }
-
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -83,7 +70,6 @@ class StateCardDisplay extends LitElement {
word-break: break-word;
display: flex;
align-items: center;
- direction: ltr;
justify-content: flex-end;
}
.state.has-unit_of_measurement {
diff --git a/src/state-summary/state-card-input_number.ts b/src/state-summary/state-card-input_number.ts
index c5b2e177ecfe..884253d3e67e 100644
--- a/src/state-summary/state-card-input_number.ts
+++ b/src/state-summary/state-card-input_number.ts
@@ -1,7 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
-import { computeRTLDirection } from "../common/util/compute_rtl";
import { debounce } from "../common/util/debounce";
import "../components/entity/state-info";
import "../components/ha-slider";
@@ -58,7 +57,6 @@ class StateCardInputNumber extends LitElement {
= 5"
- checksum: f8d20e4e18ea9cf17c95546f878213d57f3eb9fd71a01ebae1b5d1689c0da7c35f0fe39137994717e600c3d12cfaa754ab3d4cdd854a6cf4d0a907ae898bc9a5
+ checksum: 2d70f0b9fa6afc7f259877acd7e69c14f0104a69a019efb594d5de603e12b982e4a96fec5b169005fab244655951f85bff77f469d9aeadb885974f963a7d9996
languageName: node
linkType: hard
@@ -9437,10 +9437,10 @@ __metadata:
languageName: node
linkType: hard
-"hls.js@npm:1.5.2":
- version: 1.5.2
- resolution: "hls.js@npm:1.5.2"
- checksum: bf92e8f005eb4e906bc19a0baf6428df4cbfd344b517de70496b953d77f1231324d26b4dc88c375d3cd481311601c1d1d5aea34ec4e0fc96e9df22b9b0e28a84
+"hls.js@npm:1.5.3":
+ version: 1.5.3
+ resolution: "hls.js@npm:1.5.3"
+ checksum: c5b7cb6fddd6ad425a82e1b8b4f818552e6e242254b35c7d7c7b9d3beb4dd40cc85d8bbc32a5d1b57031305d7d9f4973f9d0f80b70351bb8597393f4629072fc
languageName: node
linkType: hard
@@ -9546,8 +9546,8 @@ __metadata:
"@types/tar": "npm:6.1.11"
"@types/ua-parser-js": "npm:0.7.39"
"@types/webspeechapi": "npm:0.0.29"
- "@typescript-eslint/eslint-plugin": "npm:6.19.1"
- "@typescript-eslint/parser": "npm:6.19.1"
+ "@typescript-eslint/eslint-plugin": "npm:6.20.0"
+ "@typescript-eslint/parser": "npm:6.20.0"
"@vaadin/combo-box": "npm:24.3.4"
"@vaadin/vaadin-themable-mixin": "npm:24.3.4"
"@vibrant/color": "npm:3.2.1-alpha.1"
@@ -9580,7 +9580,7 @@ __metadata:
eslint-plugin-disable: "npm:2.0.3"
eslint-plugin-import: "npm:2.29.1"
eslint-plugin-lit: "npm:1.11.0"
- eslint-plugin-lit-a11y: "npm:4.1.1"
+ eslint-plugin-lit-a11y: "npm:4.1.2"
eslint-plugin-unused-imports: "npm:3.0.0"
eslint-plugin-wc: "npm:2.0.4"
fancy-log: "npm:2.0.0"
@@ -9594,10 +9594,10 @@ __metadata:
gulp-merge-json: "npm:2.1.2"
gulp-rename: "npm:2.0.0"
gulp-zopfli-green: "npm:6.0.1"
- hls.js: "npm:1.5.2"
+ hls.js: "npm:1.5.3"
home-assistant-js-websocket: "npm:9.1.0"
html-minifier-terser: "npm:7.2.0"
- husky: "npm:9.0.6"
+ husky: "npm:9.0.7"
idb-keyval: "npm:6.2.1"
instant-mocha: "npm:1.5.2"
intl-messageformat: "npm:10.5.11"
@@ -9879,12 +9879,12 @@ __metadata:
languageName: node
linkType: hard
-"husky@npm:9.0.6":
- version: 9.0.6
- resolution: "husky@npm:9.0.6"
+"husky@npm:9.0.7":
+ version: 9.0.7
+ resolution: "husky@npm:9.0.7"
bin:
husky: bin.js
- checksum: e198c90a59d460cf860c33e0a4c3927ecfb645d4fd4c2de3fbcd5fb56b858a923af452508d549f6ed020bb48de08290912cd77c006dd2a83e551c24c17340d5b
+ checksum: c5673acb9f224b6d2b36ee4892c6b99dbe9a077b041aee6013efeacb71d2d2cbe6ab7fac061f68fa5653c7dbe93aacfeed69eea01f755688e4c36c4044dcc5c0
languageName: node
linkType: hard