Compare commits

..

4 Commits

Author SHA1 Message Date
Aidan Timson 8e1474d717 Slightly better message 2026-05-13 12:25:09 +01:00
Aidan Timson 6172c9db23 Use helper instead 2026-05-13 12:24:38 +01:00
Aidan Timson ad36772c74 Show device editor tip on name change when device exists 2026-05-13 12:15:33 +01:00
Aidan Timson f732de574b Disable update button when state is clean 2026-05-13 11:41:31 +01:00
213 changed files with 2110 additions and 2979 deletions
+1
View File
@@ -1,3 +1,4 @@
import "@material/mwc-drawer";
import "@material/mwc-top-app-bar-fixed";
import { html, css, LitElement } from "lit";
import { customElement } from "lit/decorators";
+10 -25
View File
@@ -1,3 +1,4 @@
import "@material/mwc-drawer";
import "@material/mwc-top-app-bar-fixed";
import { mdiMenu, mdiSwapHorizontal } from "@mdi/js";
import type { PropertyValues } from "lit";
@@ -6,8 +7,6 @@ import { customElement, query, state } from "lit/decorators";
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
import { setDirectionStyles } from "../../src/common/util/compute_rtl";
import "../../src/components/ha-button";
import "../../src/components/ha-drawer";
import type { HaDrawer } from "../../src/components/ha-drawer";
import { HaExpansionPanel } from "../../src/components/ha-expansion-panel";
import "../../src/components/ha-icon-button";
import "../../src/components/ha-svg-icon";
@@ -40,8 +39,8 @@ class HaGallery extends LitElement {
@query("notification-manager")
private _notifications!: HTMLElementTagNameMap["notification-manager"];
@query("ha-drawer")
private _drawer!: HaDrawer;
@query("mwc-drawer")
private _drawer!: HTMLElementTagNameMap["mwc-drawer"];
private _narrow = window.matchMedia("(max-width: 600px)").matches;
@@ -76,14 +75,15 @@ class HaGallery extends LitElement {
}
return html`
<ha-drawer
.direction=${this._rtl ? "rtl" : "ltr"}
<mwc-drawer
hasHeader
.open=${!this._narrow}
.type=${this._narrow ? "modal" : "dismissible"}
>
<div class="drawer-title">Home Assistant Design</div>
<span slot="title">Home Assistant Design</span>
<!-- <span slot="subtitle">subtitle</span> -->
<div class="sidebar">${sidebar}</div>
<div slot="appContent" class="app-content">
<div slot="appContent">
<mwc-top-app-bar-fixed>
<ha-icon-button
slot="navigationIcon"
@@ -144,7 +144,7 @@ class HaGallery extends LitElement {
</div>
</div>
</div>
</ha-drawer>
</mwc-drawer>
<notification-manager
.hass=${FAKE_HASS}
id="notifications"
@@ -226,27 +226,12 @@ class HaGallery extends LitElement {
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
--ha-sidebar-width: 256px;
}
.sidebar {
box-sizing: border-box;
max-height: calc(100vh - 64px);
overflow-y: auto;
padding: 4px;
}
.drawer-title {
align-items: center;
box-sizing: border-box;
color: var(--primary-text-color);
display: flex;
font-size: var(--ha-font-size-l);
font-weight: var(--ha-font-weight-medium);
min-height: 64px;
padding: 0 16px;
}
.sidebar a {
color: var(--primary-text-color);
display: block;
@@ -270,7 +255,7 @@ class HaGallery extends LitElement {
opacity: 0.12;
}
.app-content {
div[slot="appContent"] {
display: flex;
flex-direction: column;
min-height: 100vh;
+17 -16
View File
@@ -40,15 +40,15 @@
"@codemirror/view": "6.42.1",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.4.3",
"@formatjs/intl-displaynames": "7.3.6",
"@formatjs/intl-durationformat": "0.10.9",
"@formatjs/intl-getcanonicallocales": "3.2.7",
"@formatjs/intl-listformat": "8.3.6",
"@formatjs/intl-locale": "5.3.6",
"@formatjs/intl-numberformat": "9.3.6",
"@formatjs/intl-pluralrules": "6.3.6",
"@formatjs/intl-relativetimeformat": "12.3.6",
"@formatjs/intl-datetimeformat": "7.4.2",
"@formatjs/intl-displaynames": "7.3.5",
"@formatjs/intl-durationformat": "0.10.8",
"@formatjs/intl-getcanonicallocales": "3.2.6",
"@formatjs/intl-listformat": "8.3.5",
"@formatjs/intl-locale": "5.3.5",
"@formatjs/intl-numberformat": "9.3.5",
"@formatjs/intl-pluralrules": "6.3.5",
"@formatjs/intl-relativetimeformat": "12.3.5",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
"@fullcalendar/interaction": "6.1.20",
@@ -64,6 +64,7 @@
"@lit/reactive-element": "2.1.2",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-base": "0.27.0",
"@material/mwc-drawer": "0.27.0",
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"@material/mwc-top-app-bar": "0.27.0",
@@ -99,7 +100,7 @@
"hls.js": "1.6.16",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "11.2.5",
"intl-messageformat": "11.2.4",
"js-yaml": "4.1.1",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -141,8 +142,8 @@
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.11",
"@rspack/core": "2.0.3",
"@rsdoctor/rspack-plugin": "1.5.10",
"@rspack/core": "2.0.2",
"@rspack/dev-server": "2.0.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.26",
@@ -161,7 +162,7 @@
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.1.6",
"@vitest/coverage-v8": "4.1.5",
"babel-loader": "10.1.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.4",
@@ -170,7 +171,7 @@
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.11",
"eslint-plugin-import-x": "4.16.2",
"eslint-plugin-lit": "2.3.1",
"eslint-plugin-lit": "2.2.1",
"eslint-plugin-lit-a11y": "5.1.1",
"eslint-plugin-unused-imports": "4.4.1",
"eslint-plugin-wc": "3.1.0",
@@ -202,9 +203,9 @@
"terser-webpack-plugin": "5.6.0",
"ts-lit-plugin": "2.0.2",
"typescript": "6.0.3",
"typescript-eslint": "8.59.3",
"typescript-eslint": "8.59.2",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.6",
"vitest": "4.1.5",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
+3 -4
View File
@@ -54,8 +54,6 @@ export class HaAuthFlow extends LitElement {
@query("ha-auth-form") private _form?: HaAuthForm;
@query("ha-form") private _haForm?: HTMLElement;
createRenderRoot() {
return this;
}
@@ -162,8 +160,9 @@ export class HaAuthFlow extends LitElement {
// 100ms to give all the form elements time to initialize.
setTimeout(() => {
if (this._haForm) {
(this._haForm as any).focus();
const form = this.renderRoot.querySelector("ha-form");
if (form) {
(form as any).focus();
}
}, 100);
}
+6
View File
@@ -5,6 +5,7 @@ import { isComponentLoaded } from "./is_component_loaded";
export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
(isCore(page) || isLoadedIntegration(hass, page)) &&
!hideAdvancedPage(hass, page) &&
isNotLoadedIntegration(hass, page);
export const isLoadedIntegration = (
@@ -26,3 +27,8 @@ export const isNotLoadedIntegration = (
);
export const isCore = (page: PageNavigation) => page.core;
export const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
export const userWantsAdvanced = (hass: HomeAssistant) =>
hass.userData?.showAdvanced;
export const hideAdvancedPage = (hass: HomeAssistant, page: PageNavigation) =>
isAdvancedPage(page) && !userWantsAdvanced(hass);
@@ -1,40 +0,0 @@
import type { TooltipPositionCallback } from "echarts/types/dist/shared";
export const TOOLTIP_GAP_PX = 12;
export const TOOLTIP_TOP_OFFSET_PX = 10;
/**
* Pins the tooltip near the top of the chart and offsets it horizontally
* from the cursor so it never covers the data point being inspected.
* For axis-trigger time-series tooltips where the cursor's Y is uncorrelated
* with the displayed content.
*/
export const sideTooltipPosition: TooltipPositionCallback = (
point,
_params,
dom,
_rect,
size
) => {
const [cursorX] = point;
const [viewW, viewH] = size.viewSize;
const [tipW, tipH] = size.contentSize;
const rtl =
dom instanceof HTMLElement && getComputedStyle(dom).direction === "rtl";
const rightOfCursor = cursorX + TOOLTIP_GAP_PX;
const leftOfCursor = cursorX - TOOLTIP_GAP_PX - tipW;
let x = rtl ? leftOfCursor : rightOfCursor;
const overflowsRight = x + tipW > viewW;
const overflowsLeft = x < 0;
if (overflowsRight || overflowsLeft) {
x = rtl ? rightOfCursor : leftOfCursor;
}
x = Math.max(0, Math.min(x, viewW - tipW));
const y = Math.max(0, Math.min(TOOLTIP_TOP_OFFSET_PX, viewH - tipH));
return [x, y];
};
+3 -4
View File
@@ -19,7 +19,7 @@ import type {
} from "echarts/types/dist/shared";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { ensureArray } from "../../common/array/ensure-array";
@@ -102,8 +102,6 @@ export class HaChartBase extends LitElement {
@state() private _hiddenDatasets = new Set<string>();
@query(".chart") private _chartContainer?: HTMLDivElement;
private _modifierPressed = false;
private _isTouchDevice = "ontouchstart" in window;
@@ -471,6 +469,7 @@ export class HaChartBase extends LitElement {
private async _setupChart() {
if (this._loading) return;
const container = this.renderRoot.querySelector(".chart") as HTMLDivElement;
this._loading = true;
try {
if (this.chart) {
@@ -485,7 +484,7 @@ export class HaChartBase extends LitElement {
const style = getComputedStyle(this);
echarts.registerTheme("custom", this._createTheme(style));
this.chart = echarts.init(this._chartContainer!, "custom");
this.chart = echarts.init(container, "custom");
this.chart.on("datazoom", (e: any) => {
this._handleDataZoomEvent(e);
});
@@ -11,7 +11,6 @@ import { computeRTL } from "../../common/util/compute_rtl";
import type { LineChartEntity, LineChartState } from "../../data/history";
import type { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import type { ECOption } from "../../resources/echarts/echarts";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import {
@@ -414,7 +413,8 @@ export class StateHistoryChartLine extends LitElement {
tooltip: {
trigger: "axis",
renderMode: "html",
position: sideTooltipPosition,
position: "bottom",
align: "center",
confine: true,
formatter: this._renderTooltip,
},
@@ -14,7 +14,6 @@ import { computeRTL } from "../../common/util/compute_rtl";
import type { TimelineEntity } from "../../data/history";
import type { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import { computeTimelineColor } from "./timeline-color";
import type { ECOption } from "../../resources/echarts/echarts";
import echarts from "../../resources/echarts/echarts";
@@ -264,7 +263,8 @@ export class StateHistoryChartTimeline extends LitElement {
},
tooltip: {
renderMode: "html",
position: sideTooltipPosition,
position: "bottom",
align: "center",
confine: true,
formatter: this._renderTooltip,
},
+11 -14
View File
@@ -2,13 +2,7 @@ import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiRestart } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import {
customElement,
eventOptions,
property,
queryAll,
state,
} from "lit/decorators";
import { customElement, eventOptions, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import type {
@@ -110,11 +104,6 @@ export class StateHistoryCharts extends LitElement {
@state() private _hasZoomedCharts = false;
@queryAll("state-history-chart-line, state-history-chart-timeline")
private _chartComponents!: NodeListOf<
StateHistoryChartLine | StateHistoryChartTimeline
>;
private _isSyncing = false;
// @ts-ignore
@@ -338,7 +327,11 @@ export class StateHistoryCharts extends LitElement {
this._isSyncing = true;
requestAnimationFrame(() => {
this._chartComponents.forEach((chartComponent, index) => {
const chartComponents = this.renderRoot.querySelectorAll(
"state-history-chart-line, state-history-chart-timeline"
) as unknown as (StateHistoryChartLine | StateHistoryChartTimeline)[];
chartComponents.forEach((chartComponent, index) => {
if (index === sourceChartIndex) {
return;
}
@@ -357,7 +350,11 @@ export class StateHistoryCharts extends LitElement {
this._isSyncing = true;
requestAnimationFrame(() => {
this._chartComponents.forEach((chartComponent: any) => {
const chartComponents = this.renderRoot.querySelectorAll(
"state-history-chart-line, state-history-chart-timeline"
);
chartComponents.forEach((chartComponent: any) => {
const chartBase =
chartComponent.renderRoot?.querySelector("ha-chart-base");
+3 -4
View File
@@ -39,7 +39,6 @@ import type { HomeAssistant } from "../../types";
import { getPeriodicAxisLabelConfig } from "./axis-label";
import type { CustomLegendOption } from "./ha-chart-base";
import "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import { fillDataGapsAndRoundCaps } from "./round-caps";
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
@@ -143,8 +142,7 @@ export class StatisticsChart extends LitElement {
changedProps.has("statTypes") ||
changedProps.has("chartType") ||
changedProps.has("hideLegend") ||
changedProps.has("_hiddenStats") ||
changedProps.has("names")
changedProps.has("_hiddenStats")
) {
this._generateData();
}
@@ -461,7 +459,8 @@ export class StatisticsChart extends LitElement {
tooltip: {
trigger: "axis",
renderMode: "html",
position: sideTooltipPosition,
position: "bottom",
align: "center",
confine: true,
formatter: this._renderTooltip,
},
@@ -24,7 +24,6 @@ import "../ha-icon-button";
import "../ha-icon-button-next";
import "../ha-icon-button-prev";
import "../ha-textarea";
import type { HaTextArea } from "../ha-textarea";
import "./date-range-picker";
export type DateRangePickerRanges = Record<string, [Date, Date]>;
@@ -99,8 +98,6 @@ export class HaDateRangePicker extends LitElement {
@query(".container") private _containerElement?: HTMLDivElement;
@query("ha-textarea") private _textareaElement?: HaTextArea;
private _narrow = false;
private _unsubscribeTinyKeys?: () => void;
@@ -338,8 +335,9 @@ export class HaDateRangePicker extends LitElement {
};
private _setTextareaFocusStyle(focused: boolean) {
if (this._textareaElement) {
this._textareaElement.setFocused(focused);
const textarea = this.renderRoot.querySelector("ha-textarea");
if (textarea) {
textarea.setFocused(focused);
}
}
+2 -9
View File
@@ -142,7 +142,6 @@ export class HaStatisticPicker extends LitElement {
private async _getStatisticIds() {
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
this._picker?.requestUpdate();
this._valueRenderer = this._makeValueRenderer();
}
private _getItems = () =>
@@ -318,7 +317,7 @@ export class HaStatisticPicker extends LitElement {
}
);
private _renderValue(value: string) {
private _valueRenderer: PickerValueRenderer = (value) => {
const statisticId = value;
const item = this._computeItem(statisticId);
@@ -342,13 +341,7 @@ export class HaStatisticPicker extends LitElement {
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
`;
}
private _makeValueRenderer(): PickerValueRenderer {
return (value) => this._renderValue(value);
}
private _valueRenderer: PickerValueRenderer = this._makeValueRenderer();
};
private _computeItem(statisticId: string): StatisticComboBoxItem {
const stateObj = this.hass.states[statisticId];
+3 -5
View File
@@ -1,6 +1,6 @@
import "@home-assistant/webawesome/dist/components/popover/popover";
import { css, html, nothing, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { ScrollLockMixin } from "../mixins/scroll-lock-mixin";
@@ -25,8 +25,6 @@ export class HaAdaptivePopover extends ScrollLockMixin(HaAdaptiveDialog) {
@state() private _shouldRenderPopover = false;
@query("wa-popover") private _popoverElement?: HTMLElement;
protected willUpdate(changedProperties: PropertyValues<this>) {
if (
changedProperties.has("dialogAnchor") ||
@@ -190,7 +188,7 @@ export class HaAdaptivePopover extends ScrollLockMixin(HaAdaptiveDialog) {
}
private _handlePopoverPointerDown(ev: PointerEvent) {
const popover = this._popoverElement;
const popover = this.renderRoot.querySelector("wa-popover");
const dialog = popover?.shadowRoot?.querySelector(
"dialog"
) as HTMLDialogElement | null;
@@ -217,7 +215,7 @@ export class HaAdaptivePopover extends ScrollLockMixin(HaAdaptiveDialog) {
}
private _pulsePopover() {
const popover = this._popoverElement;
const popover = this.renderRoot.querySelector("wa-popover");
const popup = popover?.shadowRoot?.querySelector("wa-popup") as {
popup?: HTMLElement;
} | null;
+12 -10
View File
@@ -5,10 +5,10 @@ import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import type { Analytics, AnalyticsPreferences } from "../data/analytics";
import { haStyle } from "../resources/styles";
import "./ha-md-list-item";
import "./ha-switch";
import type { HaSwitch } from "./ha-switch";
import "./ha-tooltip";
import "./item/ha-row-item";
import type { HaSwitch } from "./ha-switch";
const ADDITIONAL_PREFERENCES = ["usage", "statistics"] as const;
@@ -33,7 +33,7 @@ export class HaAnalytics extends LitElement {
const baseEnabled = !loading && this.analytics!.preferences.base;
return html`
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.title`
@@ -52,10 +52,10 @@ export class HaAnalytics extends LitElement {
.disabled=${loading}
name="base"
></ha-switch>
</ha-row-item>
</ha-md-list-item>
${ADDITIONAL_PREFERENCES.map(
(preference) => html`
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.title`
@@ -81,10 +81,10 @@ export class HaAnalytics extends LitElement {
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
)}
</ha-tooltip>`}
</ha-row-item>
</ha-md-list-item>
`
)}
<ha-row-item>
<ha-md-list-item>
<span slot="headline"
>${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.title`
@@ -103,7 +103,7 @@ export class HaAnalytics extends LitElement {
.disabled=${loading}
name="diagnostics"
></ha-switch>
</ha-row-item>
</ha-md-list-item>
`;
}
@@ -139,8 +139,10 @@ export class HaAnalytics extends LitElement {
color: var(--error-color);
}
ha-row-item {
--ha-row-item-padding-inline: 0;
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
}
`,
];
+1 -4
View File
@@ -9,7 +9,6 @@ import {
customElement,
property,
query,
queryAll,
state as litState,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -32,8 +31,6 @@ export class HaAnsiToHtml extends LitElement {
@query("pre") private _pre?: HTMLPreElement;
@queryAll("div") private _divs!: NodeListOf<HTMLDivElement>;
@litState() private _filter = "";
protected render(): TemplateResult {
@@ -323,7 +320,7 @@ export class HaAnsiToHtml extends LitElement {
*/
filterLines(filter: string): boolean {
this._filter = filter;
const lines = this._divs;
const lines = this.shadowRoot?.querySelectorAll("div") || [];
let numberOfFoundLines = 0;
if (!filter) {
lines.forEach((line) => {
+4 -5
View File
@@ -1,6 +1,6 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
@@ -24,12 +24,11 @@ class HaBluePrintPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-select") private _select?: HTMLElement;
public open() {
if (this._select) {
const select = this.shadowRoot?.querySelector("ha-select");
if (select) {
// @ts-expect-error
this._select.menuOpen = true;
select.menuOpen = true;
}
}
+5 -5
View File
@@ -75,8 +75,6 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
@query("#body") private _bodyElement!: HTMLDivElement;
@query("[autofocus]") private _autofocusElement?: HTMLElement;
protected get scrollableElement(): HTMLElement | null {
return this._bodyElement;
}
@@ -95,12 +93,12 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
await this.updateComplete;
requestAnimationFrame(() => {
const element = this._autofocusElement;
if (
this._hassConfig?.auth.external &&
isIosApp(this._hassConfig.auth.external)
) {
if (element) {
const element = this.renderRoot.querySelector("[autofocus]");
if (element !== null) {
if (!element.id) {
element.id = "ha-bottom-sheet-autofocus";
}
@@ -113,7 +111,9 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
}
return;
}
element?.focus();
(
this.renderRoot.querySelector("[autofocus]") as HTMLElement | null
)?.focus();
});
};
+66 -98
View File
@@ -27,7 +27,6 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, ReactiveElement, render } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { ContextType } from "@lit/context";
import { consume } from "@lit/context";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
@@ -44,14 +43,7 @@ import type {
import type { HomeAssistant } from "../types";
import { showToast } from "../util/toast";
import { documentationUrl } from "../util/documentation-url";
import {
internationalizationContext,
registriesContext,
statesContext,
labelsContext,
configContext,
formattersContext,
} from "../data/context";
import { labelsContext } from "../data/context";
import type { LabelRegistryEntry } from "../data/label/label_registry";
import "./ha-code-editor-completion-items";
import type { CompletionItem } from "./ha-code-editor-completion-items";
@@ -86,6 +78,8 @@ export class HaCodeEditor extends ReactiveElement {
@property() public mode = "yaml";
public hass?: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@@ -129,29 +123,9 @@ export class HaCodeEditor extends ReactiveElement {
@state() private _canCopy = false;
@state()
@consume({ context: configContext, subscribe: true })
private _config?: ContextType<typeof configContext>;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: labelsContext, subscribe: true })
private _labels?: ContextType<typeof labelsContext>;
@state()
@consume({ context: registriesContext, subscribe: true })
private _registries?: ContextType<typeof registriesContext>;
@state()
@consume({ context: formattersContext, subscribe: true })
private _formatters?: ContextType<typeof formattersContext>;
@state()
@consume({ context: statesContext, subscribe: true })
private _states?: ContextType<typeof statesContext>;
private _labels?: LabelRegistryEntry[];
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
private _loadedCodeMirror?: typeof import("../resources/codemirror");
@@ -188,7 +162,6 @@ export class HaCodeEditor extends ReactiveElement {
this.codemirror.state,
[this._loadedCodeMirror.tags.comment]
);
// eslint-disable-next-line lit/prefer-query-decorators
return !!this.renderRoot.querySelector(`span.${className}`);
}
@@ -216,9 +189,9 @@ export class HaCodeEditor extends ReactiveElement {
const line = doc.lineAt(pos);
const message = `${
err.reason ||
this._i18n?.localize("ui.components.yaml-editor.error") ||
this.hass?.localize("ui.components.yaml-editor.error") ||
"YAML syntax error"
}${err.mark ? ` (${this._i18n?.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
}${err.mark ? ` (${this.hass?.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
diagnostics = [{ from: pos, to: line.to, severity: "error", message }];
}
this.codemirror.dispatch(
@@ -423,8 +396,8 @@ export class HaCodeEditor extends ReactiveElement {
this._loadedCodeMirror!.haJinjaHoverSource(
view,
pos,
this._config ? documentationUrl(this._config, "") : undefined,
this._hassArgHoverContext()
this.hass ? documentationUrl(this.hass, "") : undefined,
this.hass ? this._hassArgHoverContext() : undefined
),
{ hoverTime: 300 }
),
@@ -435,7 +408,7 @@ export class HaCodeEditor extends ReactiveElement {
const completionSources: CompletionSource[] = [
this._loadedCodeMirror.haJinjaCompletionSource,
];
if (this.autocompleteEntities) {
if (this.autocompleteEntities && this.hass) {
completionSources.push(this._entityCompletions.bind(this));
}
if (this.autocompleteIcons) {
@@ -474,12 +447,12 @@ export class HaCodeEditor extends ReactiveElement {
private _fullscreenLabel(): string {
if (this._isFullscreen) {
return (
this._i18n?.localize("ui.components.yaml-editor.exit_fullscreen") ||
this.hass?.localize("ui.components.yaml-editor.exit_fullscreen") ||
"Exit fullscreen"
);
}
return (
this._i18n?.localize("ui.components.yaml-editor.enter_fullscreen") ||
this.hass?.localize("ui.components.yaml-editor.enter_fullscreen") ||
"Enter fullscreen"
);
}
@@ -534,7 +507,7 @@ export class HaCodeEditor extends ReactiveElement {
{
id: "test",
label:
this._i18n?.localize(
this.hass?.localize(
`ui.components.yaml-editor.test_${this.testing ? "off" : "on"}`
) || "Test",
path: this.testing ? mdiBugOutline : mdiBug,
@@ -545,14 +518,14 @@ export class HaCodeEditor extends ReactiveElement {
{
id: "undo",
disabled: !this._canUndo,
label: this._i18n?.localize("ui.common.undo") || "Undo",
label: this.hass?.localize("ui.common.undo") || "Undo",
path: mdiUndo,
action: (e: Event) => this._handleUndoClick(e),
},
{
id: "redo",
disabled: !this._canRedo,
label: this._i18n?.localize("ui.common.redo") || "Redo",
label: this.hass?.localize("ui.common.redo") || "Redo",
path: mdiRedo,
action: (e: Event) => this._handleRedoClick(e),
},
@@ -560,7 +533,7 @@ export class HaCodeEditor extends ReactiveElement {
id: "copy",
disabled: !this._canCopy,
label:
this._i18n?.localize("ui.components.yaml-editor.copy_to_clipboard") ||
this.hass?.localize("ui.components.yaml-editor.copy_to_clipboard") ||
"Copy to Clipboard",
path: mdiContentCopy,
action: (e: Event) => this._handleClipboardClick(e),
@@ -568,7 +541,7 @@ export class HaCodeEditor extends ReactiveElement {
{
id: "find-replace",
label:
this._i18n?.localize("ui.components.yaml-editor.find_and_replace") ||
this.hass?.localize("ui.components.yaml-editor.find_and_replace") ||
"Find and replace",
path: mdiFindReplace,
action: (e: Event) => this._handleFindReplaceClick(e),
@@ -610,7 +583,7 @@ export class HaCodeEditor extends ReactiveElement {
await copyToClipboard(this.value);
showToast(this, {
message:
this._i18n?.localize("ui.common.copied_clipboard") ||
this.hass?.localize("ui.common.copied_clipboard") ||
"Copied to clipboard",
});
}
@@ -678,11 +651,12 @@ export class HaCodeEditor extends ReactiveElement {
};
/**
* Builds a HassArgHoverContext from the context objects so that
* Builds a HassArgHoverContext from the current hass object so that
* haJinjaHoverSource can resolve entity / device / area friendly names
* without importing the full HomeAssistant type into the resource file.
*/
private _hassArgHoverContext(): HassArgHoverContext {
const hass = this.hass!;
const labelMap: Record<
string,
{ name: string; description?: string | null }
@@ -694,33 +668,27 @@ export class HaCodeEditor extends ReactiveElement {
};
}
return {
states: this._states as HassArgHoverContext["states"],
devices: this._registries?.devices as HassArgHoverContext["devices"],
areas: this._registries?.areas as HassArgHoverContext["areas"],
floors: this._registries?.floors as HassArgHoverContext["floors"],
entities: this._registries?.entities as HassArgHoverContext["entities"],
states: hass.states as HassArgHoverContext["states"],
devices: hass.devices as HassArgHoverContext["devices"],
areas: hass.areas as HassArgHoverContext["areas"],
floors: hass.floors as HassArgHoverContext["floors"],
entities: hass.entities as HassArgHoverContext["entities"],
labels: labelMap,
formatEntityState: (entityId) =>
this._formatters!.formatEntityState(this._states![entityId]),
hass.formatEntityState(hass.states[entityId]),
formatEntityName: (entityId) => {
const stateObj = this._states?.[entityId];
const stateObj = hass.states[entityId];
return (
(stateObj?.attributes.friendly_name as string | undefined) ??
this._registries?.entities?.[entityId]?.name ??
hass.entities[entityId]?.name ??
undefined
);
},
formatAttributeName: (entityId, attribute) =>
this._formatters!.formatEntityAttributeName(
this._states![entityId],
attribute
),
hass.formatEntityAttributeName(hass.states[entityId], attribute),
formatAttributeValue: (entityId, attribute) =>
this._formatters!.formatEntityAttributeValue(
this._states![entityId],
attribute
),
localize: (key) => this._i18n!.localize(key as never),
hass.formatEntityAttributeValue(hass.states[entityId], attribute),
localize: (key) => hass.localize(key as never),
};
}
@@ -730,51 +698,49 @@ export class HaCodeEditor extends ReactiveElement {
? completion.apply
: completion.label;
const context = getEntityContext(
this._states![key],
this._registries!.entities,
this._registries!.devices,
this._registries!.areas,
this._registries!.floors
this.hass!.states[key],
this.hass!.entities,
this.hass!.devices,
this.hass!.areas,
this.hass!.floors
);
const completionInfo = document.createElement("div");
completionInfo.classList.add("completion-info");
const formattedState = this._formatters!.formatEntityState(
this._states![key]
);
const formattedState = this.hass!.formatEntityState(this.hass!.states[key]);
const completionItems: CompletionItem[] = [
{
label: this._i18n!.localize(
label: this.hass!.localize(
"ui.components.entity.entity-state-picker.state"
),
value: formattedState,
subValue:
// If the state exactly matches the formatted state, don't show the raw state
this._states![key].state === formattedState
this.hass!.states[key].state === formattedState
? undefined
: this._states![key].state,
: this.hass!.states[key].state,
},
];
if (context.device && context.device.name) {
completionItems.push({
label: this._i18n!.localize("ui.components.device-picker.device"),
label: this.hass!.localize("ui.components.device-picker.device"),
value: context.device.name,
});
}
if (context.area && context.area.name) {
completionItems.push({
label: this._i18n!.localize("ui.components.area-picker.area"),
label: this.hass!.localize("ui.components.area-picker.area"),
value: context.area.name,
});
}
if (context.floor && context.floor.name) {
completionItems.push({
label: this._i18n!.localize("ui.components.floor-picker.floor"),
label: this.hass!.localize("ui.components.floor-picker.floor"),
value: context.floor.name,
});
}
@@ -795,15 +761,15 @@ export class HaCodeEditor extends ReactiveElement {
entityId: string,
attribute: string
): CompletionInfo | null => {
if (!this._states || !this._formatters) return null;
const stateObj = this._states[entityId];
if (!this.hass) return null;
const stateObj = this.hass.states[entityId];
if (!stateObj) return null;
const translatedName = this._formatters.formatEntityAttributeName(
const translatedName = this.hass.formatEntityAttributeName(
stateObj,
attribute
);
const formattedValue = this._formatters.formatEntityAttributeValue(
const formattedValue = this.hass.formatEntityAttributeValue(
stateObj,
attribute
);
@@ -843,9 +809,9 @@ export class HaCodeEditor extends ReactiveElement {
completion: Completion
): CompletionInfo | Promise<CompletionInfo> | null => {
if (
this._states &&
this.hass &&
typeof completion.apply === "string" &&
completion.apply in this._states
completion.apply in this.hass.states
) {
return this._renderInfo(completion);
}
@@ -1054,7 +1020,7 @@ export class HaCodeEditor extends ReactiveElement {
private _statesDotNotationCompletions(
context: CompletionContext
): CompletionResult | null | undefined {
if (!this._states) return undefined;
if (!this.hass) return undefined;
const { state: editorState, pos } = context;
const tree = this._loadedCodeMirror!.syntaxTree(editorState);
@@ -1163,7 +1129,9 @@ export class HaCodeEditor extends ReactiveElement {
case 0: {
// states. → offer all unique domains
const domains = [
...new Set(Object.keys(this._states).map((id) => id.split(".")[0])),
...new Set(
Object.keys(this.hass.states).map((id) => id.split(".")[0])
),
].sort();
return {
from: completionFrom,
@@ -1174,7 +1142,7 @@ export class HaCodeEditor extends ReactiveElement {
case 1: {
// states.<domain>. → offer entity object_ids for that domain
const [domain] = segments;
const entities = Object.keys(this._states)
const entities = Object.keys(this.hass.states)
.filter((id) => id.startsWith(`${domain}.`))
.map((id) => id.split(".").slice(1).join("."));
if (!entities.length) return { from: completionFrom, options: [] };
@@ -1204,7 +1172,7 @@ export class HaCodeEditor extends ReactiveElement {
}
// Offer attribute names from the entity's state object
const entityId = `${domain}.${entity}`;
const entityState = this._states[entityId];
const entityState = this.hass.states[entityId];
if (!entityState) return { from: completionFrom, options: [] };
const attrNames = Object.keys(entityState.attributes).sort();
return {
@@ -1374,8 +1342,8 @@ export class HaCodeEditor extends ReactiveElement {
): CompletionResult {
const from = stringNode.from + 1;
const empty: CompletionResult = { from, options: [] };
if (!entityId || !this._states) return empty;
const entityState = this._states[entityId];
if (!entityId || !this.hass) return empty;
const entityState = this.hass.states[entityId];
if (!entityState) return empty;
const attrs = Object.keys(entityState.attributes).sort();
if (!attrs.length) return empty;
@@ -1395,7 +1363,7 @@ export class HaCodeEditor extends ReactiveElement {
from: number;
to: number;
}): CompletionResult | null {
const states = this._getStates(this._states!);
const states = this._getStates(this.hass!.states);
if (!states?.length) return null;
// from is stringNode.from + 1 to skip the opening quote character.
const from = stringNode.from + 1;
@@ -1429,8 +1397,8 @@ export class HaCodeEditor extends ReactiveElement {
from: number;
to: number;
}): CompletionResult | null {
if (!this._registries?.devices) return null;
const devices = this._getDevices(this._registries.devices);
if (!this.hass?.devices) return null;
const devices = this._getDevices(this.hass.devices);
if (!devices.length) return null;
return {
from: stringNode.from + 1,
@@ -1458,8 +1426,8 @@ export class HaCodeEditor extends ReactiveElement {
from: number;
to: number;
}): CompletionResult | null {
if (!this._registries?.areas) return null;
const areas = this._getAreas(this._registries.areas);
if (!this.hass?.areas) return null;
const areas = this._getAreas(this.hass.areas);
if (!areas.length) return null;
return {
from: stringNode.from + 1,
@@ -1487,8 +1455,8 @@ export class HaCodeEditor extends ReactiveElement {
from: number;
to: number;
}): CompletionResult | null {
if (!this._registries?.floors) return null;
const floors = this._getFloors(this._registries.floors);
if (!this.hass?.floors) return null;
const floors = this._getFloors(this.hass.floors);
if (!floors.length) return null;
return {
from: stringNode.from + 1,
@@ -1588,7 +1556,7 @@ export class HaCodeEditor extends ReactiveElement {
// If cursor is after the entity field, show all entities
if (context.pos >= afterField) {
const states = this._getStates(this._states!);
const states = this._getStates(this.hass!.states);
if (!states || !states.length) {
return null;
@@ -1643,7 +1611,7 @@ export class HaCodeEditor extends ReactiveElement {
const afterListMarker = currentLine.from + listItemMatch[0].length;
if (context.pos >= afterListMarker) {
const states = this._getStates(this._states!);
const states = this._getStates(this.hass!.states);
if (!states || !states.length) {
return null;
@@ -1703,7 +1671,7 @@ export class HaCodeEditor extends ReactiveElement {
return null;
}
const states = this._getStates(this._states!);
const states = this._getStates(this.hass!.states);
if (!states || !states.length) {
return null;
-1
View File
@@ -54,7 +54,6 @@ export class HaControlSelect extends LitElement {
this._activeIndex = index;
this.requestUpdate();
this.updateComplete.then(() => {
// eslint-disable-next-line lit/prefer-query-decorators
const option = this.shadowRoot?.querySelector(
`#option-${this.options![index].value}`
) as HTMLElement;
+125 -265
View File
@@ -1,115 +1,36 @@
import "@home-assistant/webawesome/dist/components/drawer/drawer";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, query } from "lit/decorators";
import { DrawerBase } from "@material/mwc-drawer/mwc-drawer-base";
import { styles } from "@material/mwc-drawer/mwc-drawer.css";
import type { PropertyValues } from "lit";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { HASSDomEvent } from "../common/dom/fire_event";
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
declare global {
interface HASSDomEvents {
"hass-drawer-closed": undefined;
"hass-layout-transition": { active: boolean; reason?: string };
}
interface HTMLElementEventMap {
"hass-drawer-closed": HASSDomEvent<HASSDomEvents["hass-drawer-closed"]>;
"hass-layout-transition": HASSDomEvent<
HASSDomEvents["hass-layout-transition"]
>;
}
}
const blockingElements = (document as any).$blockingElements;
@customElement("ha-drawer")
export class HaDrawer extends LitElement {
private static readonly _SWIPE_AXIS_TOLERANCE = 32;
export class HaDrawer extends DrawerBase {
@property() public direction: "ltr" | "rtl" = "ltr";
@property({ reflect: true }) public direction: "ltr" | "rtl" = "ltr";
private _mc?: HammerManager;
@property({ reflect: true }) public type: "" | "dismissible" | "modal" = "";
@property({ type: Boolean, reflect: true }) public open = false;
@query("wa-drawer") private _modalDrawer?: HTMLElement;
@query(".sidebar-shell") private _sidebarShell?: HTMLElement;
private _rtlStyle?: HTMLElement;
private _sidebarTransitionActive = false;
private _transitionTarget?: HTMLElement;
private _gestureRecognizer = new SwipeGestureRecognizer({
velocitySwipeThreshold: 0.35,
});
private _touchStartY = 0;
private _touchDeltaY = 0;
private get _modal() {
return this.type === "modal";
}
protected render(): TemplateResult {
return this._modal
? html`
<slot name="appContent"></slot>
<wa-drawer
placement="start"
.open=${this.open}
light-dismiss
without-header
@touchstart=${this._handleTouchStart}
@wa-after-hide=${this._handleAfterHide}
>
<slot></slot>
</wa-drawer>
`
: html`
<div class="layout">
<div class="sidebar-shell">
<slot></slot>
</div>
<div class="app-content">
<slot name="appContent"></slot>
</div>
</div>
`;
}
protected updated(_: PropertyValues<this>) {
this._syncTransitionListeners();
if (!this.open) {
this._resetSwipeTracking();
}
}
protected firstUpdated() {
this._syncTransitionListeners();
}
public disconnectedCallback() {
super.disconnectedCallback();
this._removeTransitionListeners();
this._unregisterSwipeHandlers();
}
private _handleAfterHide(ev: Event) {
ev.stopPropagation();
this.open = false;
fireEvent(this, "hass-drawer-closed");
}
private _closeModalDrawer() {
this.open = false;
}
private _handleDrawerTransitionStart = (ev: TransitionEvent) => {
if (
ev.propertyName !==
(this.type === "dismissible" ? "transform" : "width") ||
this._sidebarTransitionActive
) {
if (ev.propertyName !== "width" || this._sidebarTransitionActive) {
return;
}
this._sidebarTransitionActive = true;
@@ -120,11 +41,7 @@ export class HaDrawer extends LitElement {
};
private _handleDrawerTransitionEnd = (ev: TransitionEvent) => {
if (
ev.propertyName !==
(this.type === "dismissible" ? "transform" : "width") ||
!this._sidebarTransitionActive
) {
if (ev.propertyName !== "width" || !this._sidebarTransitionActive) {
return;
}
this._sidebarTransitionActive = false;
@@ -134,207 +51,150 @@ export class HaDrawer extends LitElement {
});
};
private _handleTouchStart = (ev: TouchEvent) => {
if (!this._modal || !this.open) {
return;
}
const drawer = this._modalDrawer;
const dialog = drawer?.shadowRoot?.querySelector(
"dialog"
) as HTMLDialogElement | null;
if (!dialog) {
return;
}
const path = ev.composedPath();
if (!path.includes(dialog)) {
return;
}
ev.stopPropagation();
this._startSwipeTracking(ev.touches[0].clientX, ev.touches[0].clientY);
};
private _startSwipeTracking(clientX: number, clientY: number) {
document.addEventListener("touchmove", this._handleTouchMove, {
passive: true,
});
document.addEventListener("touchend", this._handleTouchEnd);
document.addEventListener("touchcancel", this._handleTouchEnd);
this._touchStartY = clientY;
this._touchDeltaY = 0;
this._gestureRecognizer.start(clientX);
protected createAdapter() {
return {
...super.createAdapter(),
trapFocus: () => {
blockingElements.push(this);
this.appContent.inert = true;
document.body.style.overflow = "hidden";
},
releaseFocus: () => {
blockingElements.remove(this);
this.appContent.inert = false;
document.body.style.overflow = "";
},
};
}
private _handleTouchMove = (ev: TouchEvent) => {
const currentX = ev.touches[0].clientX;
const currentY = ev.touches[0].clientY;
this._touchDeltaY = Math.abs(currentY - this._touchStartY);
this._gestureRecognizer.move(currentX);
};
protected updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
if (changedProps.has("direction")) {
this.mdcRoot.dir = this.direction;
if (this.direction === "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%);
}
`;
private _handleTouchEnd = () => {
this._unregisterSwipeHandlers();
const result = this._gestureRecognizer.end();
const isHorizontalGesture =
Math.abs(result.delta) >
this._touchDeltaY + HaDrawer._SWIPE_AXIS_TOLERANCE;
if (!isHorizontalGesture) {
this._resetSwipeTracking();
return;
}
const drawerDialog = this._modalDrawer?.shadowRoot?.querySelector(
'[part="dialog"]'
) as HTMLElement | null;
const drawerWidth = drawerDialog?.offsetWidth || 0;
if (result.isSwipe) {
const closeByVelocity =
this.direction === "rtl"
? result.isDownwardSwipe
: !result.isDownwardSwipe;
if (closeByVelocity) {
this._closeModalDrawer();
this.shadowRoot!.appendChild(this._rtlStyle);
} else if (this._rtlStyle) {
this.shadowRoot!.removeChild(this._rtlStyle);
}
return;
}
const closeByDistance =
drawerWidth > 0 &&
(this.direction === "rtl"
? result.delta > 0 && Math.abs(result.delta) > drawerWidth * 0.5
: result.delta < 0 && Math.abs(result.delta) > drawerWidth * 0.5);
if (closeByDistance) {
this._closeModalDrawer();
if (changedProps.has("open") && this.open && this.type === "modal") {
this._setupSwipe();
} else if (this._mc) {
this._mc.destroy();
this._mc = undefined;
}
};
private _unregisterSwipeHandlers() {
document.removeEventListener("touchmove", this._handleTouchMove);
document.removeEventListener("touchend", this._handleTouchEnd);
document.removeEventListener("touchcancel", this._handleTouchEnd);
}
private _resetSwipeTracking() {
this._unregisterSwipeHandlers();
this._gestureRecognizer.reset();
this._touchStartY = 0;
this._touchDeltaY = 0;
}
private _syncTransitionListeners() {
if (this._transitionTarget === this._sidebarShell) {
return;
}
this._removeTransitionListeners();
if (!this._sidebarShell) {
return;
}
this._transitionTarget = this._sidebarShell;
this._transitionTarget.addEventListener(
protected firstUpdated() {
super.firstUpdated();
this.mdcRoot?.addEventListener(
"transitionstart",
this._handleDrawerTransitionStart
);
this._transitionTarget.addEventListener(
this.mdcRoot?.addEventListener(
"transitionend",
this._handleDrawerTransitionEnd
);
this._transitionTarget.addEventListener(
this.mdcRoot?.addEventListener(
"transitioncancel",
this._handleDrawerTransitionEnd
);
}
private _removeTransitionListeners() {
if (!this._transitionTarget) {
return;
}
this._transitionTarget.removeEventListener(
public disconnectedCallback() {
super.disconnectedCallback();
this.mdcRoot?.removeEventListener(
"transitionstart",
this._handleDrawerTransitionStart
);
this._transitionTarget.removeEventListener(
this.mdcRoot?.removeEventListener(
"transitionend",
this._handleDrawerTransitionEnd
);
this._transitionTarget.removeEventListener(
this.mdcRoot?.removeEventListener(
"transitioncancel",
this._handleDrawerTransitionEnd
);
this._transitionTarget = undefined;
}
static styles = css`
:host {
display: block;
height: 100%;
}
private async _setupSwipe() {
const hammer = await import("../resources/hammer");
this._mc = new hammer.Manager(document, {
touchAction: "pan-y",
});
this._mc.add(
new hammer.Swipe({
direction:
this.direction === "rtl"
? hammer.DIRECTION_RIGHT
: hammer.DIRECTION_LEFT,
})
);
this._mc.on("swipeleft swiperight", () => {
fireEvent(this, "hass-toggle-menu", { open: false });
});
}
.layout {
height: 100%;
}
.sidebar-shell {
position: fixed;
width: var(--ha-sidebar-width);
height: 100%;
border-inline-end: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
box-sizing: border-box;
transition: width var(--ha-animation-duration-normal) ease;
}
.app-content {
overflow: unset;
min-width: 0;
padding-inline-start: var(--ha-sidebar-width);
width: 100%;
height: 100%;
box-sizing: border-box;
transition:
padding-inline-start var(--ha-animation-duration-normal) ease,
width var(--ha-animation-duration-normal) ease;
}
:host([type="dismissible"]) .sidebar-shell {
transition: transform var(--ha-animation-duration-normal) ease;
}
:host([type="dismissible"]:not([open])) .sidebar-shell {
transform: translateX(-100%);
}
:host([type="dismissible"][direction="rtl"]:not([open])) .sidebar-shell {
transform: translateX(100%);
}
:host([type="dismissible"]:not([open])) .app-content {
padding-inline-start: 0;
}
wa-drawer {
--size: var(--ha-sidebar-width, 256px);
--show-duration: var(--ha-animation-duration-normal);
--hide-duration: var(--ha-animation-duration-normal);
}
wa-drawer::part(body) {
margin: 0;
padding: 0;
}
`;
static override styles = [
styles,
css`
.mdc-drawer {
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;
transition-property: transform, width;
transition-duration:
var(--mdc-drawer-transition-duration, 0.2s),
var(--ha-animation-duration-normal);
transition-timing-function:
var(
--mdc-drawer-transition-timing-function,
cubic-bezier(0.4, 0, 0.2, 1)
),
ease;
}
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
z-index: 200;
}
.mdc-drawer-app-content {
overflow: unset;
flex: none;
padding-left: var(--mdc-drawer-width);
padding-inline-start: var(--mdc-drawer-width);
padding-inline-end: initial;
direction: var(--direction);
width: 100%;
box-sizing: border-box;
transition:
padding-left var(--ha-animation-duration-normal) ease,
padding-inline-start var(--ha-animation-duration-normal) ease;
}
@media (prefers-reduced-motion: reduce) {
/* Use 1ms instead of "none" so the transitionend event still fires.
The MDC drawer foundation relies on it to complete the close cycle. */
.mdc-drawer,
.mdc-drawer-app-content {
transition: 1ms;
}
}
`,
];
}
declare global {
+3 -4
View File
@@ -2,7 +2,7 @@ import type { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { deepEqual } from "../common/util/deep-equal";
import type { Blueprints } from "../data/blueprint";
@@ -32,8 +32,6 @@ export class HaFilterBlueprints extends LitElement {
@state() private _blueprints?: Blueprints;
@query("ha-list") private _list?: HTMLElement;
public willUpdate(properties: PropertyValues<this>) {
super.willUpdate(properties);
@@ -98,7 +96,8 @@ export class HaFilterBlueprints extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (this.narrow || !this.expanded) return;
this._list!.style.height = `${this.clientHeight - 49}px`;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - 49}px`;
}, 300);
}
}
+3 -4
View File
@@ -10,7 +10,7 @@ import {
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import type { CategoryRegistryEntry } from "../data/category_registry";
@@ -49,8 +49,6 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
@state() private _shouldRender = false;
@query("ha-list") private _list?: HTMLElement;
protected hassSubscribeRequiredHostProps = ["scope"];
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
@@ -171,7 +169,8 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this._list!.style.height = `${this.clientHeight - (49 + 48)}px`;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - (49 + 48)}px`;
}, 300);
}
}
+3 -4
View File
@@ -1,7 +1,7 @@
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
@@ -34,8 +34,6 @@ export class HaFilterDevices extends LitElement {
@state() private _filter?: string;
@query("ha-list") private _list?: HTMLElement;
public willUpdate(properties: PropertyValues<this>) {
super.willUpdate(properties);
@@ -137,7 +135,8 @@ export class HaFilterDevices extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - 49 - 4 - 32}px`;
// 49px - height of a header + 1px
// 4px - padding-top of the search-input
// 32px - height of the search input
+16 -18
View File
@@ -1,8 +1,7 @@
import type { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
@@ -32,8 +31,6 @@ export class HaFilterDomains extends LitElement {
@state() private _filter?: string;
@query("ha-list") private _list?: HTMLElement;
protected render() {
return html`
<ha-expansion-panel
@@ -65,7 +62,7 @@ export class HaFilterDomains extends LitElement {
multi
>
${repeat(
this._domains(this.hass.states, this._filter, this.value),
this._domains(this.hass.states, this._filter),
(i) => i,
(domain) =>
html`<ha-check-list-item
@@ -87,7 +84,7 @@ export class HaFilterDomains extends LitElement {
`;
}
private _domains = memoizeOne((states, filter, _value) => {
private _domains = memoizeOne((states, filter) => {
const domains = new Set<string>();
Object.keys(states).forEach((entityId) => {
domains.add(computeDomain(entityId));
@@ -112,7 +109,8 @@ export class HaFilterDomains extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - 49 - 4 - 32}px`;
// 49px - height of a header + 1px
// 4px - padding-top of the search-input
// 32px - height of the search input
@@ -128,19 +126,19 @@ export class HaFilterDomains extends LitElement {
this.expanded = ev.detail.expanded;
}
private _handleItemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
const domains = this._domains(this.hass.states, this._filter, this.value);
const visibleDomains = new Set(domains);
const preserved = (this.value || []).filter((d) => !visibleDomains.has(d));
const selected = [...ev.detail.index]
.map((i) => domains[i])
.filter((d): d is string => !!d);
this.value = [...preserved, ...selected];
private _handleItemSelected(
ev: CustomEvent<{ diff: { added: number[]; removed: number[] } }>
) {
const domains = this._domains(this.hass.states, this._filter);
if (ev.detail.diff.added.length) {
this.value = [...(this.value || []), domains[ev.detail.diff.added[0]]];
} else if (ev.detail.diff.removed.length) {
const removedDomain = domains[ev.detail.diff.removed[0]];
this.value = this.value?.filter((value) => value !== removedDomain);
}
fireEvent(this, "data-table-filter-changed", {
value: this.value.length ? this.value : undefined,
value: this.value,
items: undefined,
});
}
+3 -4
View File
@@ -1,7 +1,7 @@
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeStateDomain } from "../common/entity/compute_state_domain";
@@ -36,8 +36,6 @@ export class HaFilterEntities extends LitElement {
@state() private _filter?: string;
@query("ha-list") private _list?: HTMLElement;
public willUpdate(properties: PropertyValues<this>) {
super.willUpdate(properties);
@@ -104,7 +102,8 @@ export class HaFilterEntities extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - 49 - 4 - 32}px`;
// 49px - height of a header + 1px
// 4px - padding-top of the search-input
// 32px - height of the search input
+3 -4
View File
@@ -1,7 +1,7 @@
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
@@ -42,8 +42,6 @@ export class HaFilterFloorAreas extends LitElement {
@state() private _shouldRender = false;
@query("ha-list-selectable") private _list?: HTMLElement;
public willUpdate(properties: PropertyValues<this>) {
super.willUpdate(properties);
@@ -209,7 +207,8 @@ export class HaFilterFloorAreas extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this._list!.style.height = `${this.clientHeight - 49}px`;
this.renderRoot.querySelector("ha-list-selectable")!.style.height =
`${this.clientHeight - 49}px`;
}, 300);
}
}
+16 -14
View File
@@ -1,8 +1,7 @@
import type { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
@@ -35,8 +34,6 @@ export class HaFilterIntegrations extends LitElement {
@state() private _filter?: string;
@query("ha-list") private _list?: HTMLElement;
protected render() {
return html`
<ha-expansion-panel
@@ -101,7 +98,8 @@ export class HaFilterIntegrations extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - 49 - 4 - 32}px`;
// 49px - height of a header + 1px
// 4px - padding-top of the search-input
// 32px - height of the search input
@@ -149,7 +147,9 @@ export class HaFilterIntegrations extends LitElement {
)
);
private _itemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
private _itemSelected(
ev: CustomEvent<{ diff: { added: number[]; removed: number[] } }>
) {
const integrations = this._integrations(
this.hass.localize,
this._manifests!,
@@ -157,16 +157,18 @@ export class HaFilterIntegrations extends LitElement {
this.value
);
const visibleDomains = new Set(integrations.map((i) => i.domain));
const preserved = (this.value || []).filter((d) => !visibleDomains.has(d));
const selected = [...ev.detail.index]
.map((i) => integrations[i]?.domain)
.filter((d): d is string => !!d);
this.value = [...preserved, ...selected];
if (ev.detail.diff.added.length) {
this.value = [
...(this.value || []),
integrations[ev.detail.diff.added[0]].domain,
];
} else if (ev.detail.diff.removed.length) {
const removedDomain = integrations[ev.detail.diff.removed[0]].domain;
this.value = this.value?.filter((val) => val !== removedDomain);
}
fireEvent(this, "data-table-filter-changed", {
value: this.value.length ? this.value : undefined,
value: this.value,
items: undefined,
});
}
+3 -4
View File
@@ -3,7 +3,7 @@ import type { SelectedDetail } from "@material/mwc-list";
import { mdiCog, mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
@@ -41,8 +41,6 @@ export class HaFilterLabels extends LitElement {
@state() private _filter?: string;
@query("ha-list") private _list?: HTMLElement;
private _filteredLabels = memoizeOne(
// `_value` used to recalculate the memoization when the selection changes
(labels: LabelRegistryEntry[], filter: string | undefined, _value) =>
@@ -139,7 +137,8 @@ export class HaFilterLabels extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this._list!.style.height = `${this.clientHeight - (49 + 48 + 32 + 4)}px`;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - (49 + 48 + 32 + 4)}px`;
// 49px - height of a header + 1px
// 4px - padding-top of the search-input
// 32px - height of the search input
+3 -4
View File
@@ -2,7 +2,7 @@ import type { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
@@ -33,8 +33,6 @@ export class HaFilterVoiceAssistants extends LitElement {
@state() private _shouldRender = false;
@query("ha-list") private _list?: HTMLElement;
protected render() {
return html`
<ha-expansion-panel
@@ -95,7 +93,8 @@ export class HaFilterVoiceAssistants extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this._list!.style.height = `${this.clientHeight - 49}px`;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - 49}px`;
}, 300);
}
}
@@ -1,7 +1,7 @@
import { mdiPlus } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { stopPropagation } from "../../common/dom/stop_propagation";
import type { LocalizeFunc } from "../../common/translations/localize";
@@ -49,15 +49,14 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
@state() private _displayActions?: string[];
@query("ha-form") private _form?: HaForm;
public async focus() {
await this.updateComplete;
this._form?.focus();
this.renderRoot.querySelector("ha-form")?.focus();
}
public reportValidity(): boolean {
return this._form ? this._form.reportValidity() : true;
const form = this.renderRoot.querySelector<HaForm>("ha-form");
return form ? form.reportValidity() : true;
}
protected updated(changedProps: PropertyValues<this>): void {
+2 -4
View File
@@ -1,6 +1,6 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types";
@@ -83,10 +83,8 @@ export class HaForm extends LitElement implements HaFormElement {
delegatesFocus: true,
};
@query(".root") private _root?: HTMLElement;
public reportValidity(): boolean {
const root = this._root;
const root = this.renderRoot.querySelector(".root");
if (!root) {
return true;
}
@@ -314,7 +314,6 @@ export class HaItemDisplayEditor extends LitElement {
// refocus the item after the sort
setTimeout(async () => {
await this.updateComplete;
// eslint-disable-next-line lit/prefer-query-decorators
const selectedElement = this.shadowRoot?.querySelector(
`ha-md-list-item:nth-child(${this._dragIndex! + 1})`
) as HTMLElement | null;
@@ -188,6 +188,7 @@ export class HaObjectSelector extends LitElement {
}
return html`<ha-yaml-editor
.hass=${this.hass}
.readonly=${this.disabled}
.label=${this.label}
.required=${this.required}
@@ -101,6 +101,7 @@ export class HaTemplateSelector extends LitElement {
: nothing}
<ha-code-editor
mode="jinja2"
.hass=${this.hass}
.value=${this.value}
.readOnly=${this.disabled}
.placeholder=${this.placeholder || "{{ ... }}"}
+8 -1
View File
@@ -86,6 +86,9 @@ export class HaServiceControl extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "show-advanced", type: Boolean })
public showAdvanced = false;
@property({ attribute: "show-service-id", type: Boolean })
public showServiceId = false;
@@ -542,6 +545,7 @@ export class HaServiceControl extends LitElement {
: ""}
${shouldRenderServiceDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.components.service-control.action_data"
)}
@@ -663,7 +667,10 @@ export class HaServiceControl extends LitElement {
? this.hass.services[domain][serviceName].description_placeholders
: undefined;
return dataField.selector
return dataField.selector &&
(!dataField.advanced ||
this.showAdvanced ||
(this._value?.data && this._value.data[dataField.key] !== undefined))
? html`<ha-settings-row .narrow=${this.narrow}>
${!showOptional
? hasOptional
+4 -7
View File
@@ -2,7 +2,7 @@ import { mdiStarFourPoints } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import type {
@@ -52,10 +52,6 @@ export class HaSuggestWithAIButton extends LitElement {
@state()
private _minWidth?: string;
@query("ha-assist-chip") private _chip?: HTMLElement & {
offsetWidth: number;
};
private _intervalId?: number;
protected firstUpdated(_changedProperties: PropertyValues<this>): void {
@@ -113,8 +109,9 @@ export class HaSuggestWithAIButton extends LitElement {
}
// Capture current width before changing state
if (this._chip) {
this._minWidth = `${this._chip.offsetWidth}px`;
const chip = this.shadowRoot?.querySelector("ha-assist-chip");
if (chip) {
this._minWidth = `${chip.offsetWidth}px`;
}
// Reset to suggesting state
-1
View File
@@ -486,7 +486,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
fireEvent(this, "value-changed", { value });
// eslint-disable-next-line lit/prefer-query-decorators
this.shadowRoot
?.querySelector(
`ha-target-picker-item-group[type='${this._newTarget?.type}']`
+33 -1
View File
@@ -1,3 +1,4 @@
import "@home-assistant/webawesome/dist/components/popup/popup";
import { mdiLightbulbOutline } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
@@ -9,18 +10,41 @@ import "./ha-svg-icon";
class HaTip extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
/**
* When set, renders the tip inside a popup anchored to the given element
* instead of inline. Does not steal focus.
*/
@property({ attribute: false }) public popoverAnchor?: Element;
public render() {
if (!this.hass) {
return nothing;
}
return html`
const content = html`
<ha-svg-icon .path=${mdiLightbulbOutline}></ha-svg-icon>
<span class="prefix"
>${this.hass.localize("ui.panel.config.tips.tip")}</span
>
<span class="text"><slot></slot></span>
`;
if (this.popoverAnchor) {
return html`
<wa-popup
active
.anchor=${this.popoverAnchor}
placement="top-start"
distance="4"
flip
shift
>
<div class="popup-content">${content}</div>
</wa-popup>
`;
}
return content;
}
static styles = css`
@@ -40,6 +64,14 @@ class HaTip extends LitElement {
.prefix {
font-weight: var(--ha-font-weight-medium);
}
.popup-content {
padding: var(--ha-space-2) var(--ha-space-3);
background: var(--card-background-color);
border-radius: var(--ha-border-radius-xl);
box-shadow: var(--wa-shadow-m);
color: var(--primary-text-color);
}
`;
}
+7 -10
View File
@@ -3,16 +3,14 @@ import { DEFAULT_SCHEMA, dump, load } from "js-yaml";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type { ContextType } from "@lit/context";
import { consume } from "@lit/context";
import { fireEvent } from "../common/dom/fire_event";
import { copyToClipboard } from "../common/util/copy-clipboard";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import { showToast } from "../util/toast";
import "./ha-button";
import "./ha-code-editor";
import type { HaCodeEditor } from "./ha-code-editor";
import { internationalizationContext } from "../data/context";
const isEmpty = (obj: Record<string, unknown>): boolean => {
if (typeof obj !== "object" || obj === null) {
@@ -28,6 +26,8 @@ const isEmpty = (obj: Record<string, unknown>): boolean => {
@customElement("ha-yaml-editor")
export class HaYamlEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value?: any;
@property({ attribute: false }) public yamlSchema: Schema = DEFAULT_SCHEMA;
@@ -59,10 +59,6 @@ export class HaYamlEditor extends LitElement {
@state() private _yaml = "";
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
@query("ha-code-editor") _codeEditor?: HaCodeEditor;
public setValue(value): void {
@@ -116,6 +112,7 @@ export class HaYamlEditor extends LitElement {
? html`<p>${this.label}${this.required ? " *" : ""}</p>`
: nothing}
<ha-code-editor
.hass=${this.hass}
.value=${this._yaml}
.readOnly=${this.readOnly}
.disableFullscreen=${this.disableFullscreen}
@@ -135,7 +132,7 @@ export class HaYamlEditor extends LitElement {
${this.copyClipboard
? html`
<ha-button appearance="plain" @click=${this._copyYaml}>
${this._i18n!.localize(
${this.hass.localize(
"ui.components.yaml-editor.copy_to_clipboard"
)}
</ha-button>
@@ -166,7 +163,7 @@ export class HaYamlEditor extends LitElement {
// Invalid YAML
isValid = false;
yamlError = err;
errorMsg = `${this._i18n!.localize("ui.components.yaml-editor.error", { reason: err.reason })}${err.mark ? ` (${this._i18n!.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
errorMsg = `${this.hass.localize("ui.components.yaml-editor.error", { reason: err.reason })}${err.mark ? ` (${this.hass.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
}
} else {
parsed = {};
@@ -204,7 +201,7 @@ export class HaYamlEditor extends LitElement {
if (this.yaml) {
await copyToClipboard(this.yaml);
showToast(this, {
message: this._i18n!.localize("ui.common.copied_clipboard"),
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
}
+5 -4
View File
@@ -2,7 +2,7 @@ import { consume, type ContextType } from "@lit/context";
import { mdiDeleteOutline, mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../common/dom/fire_event";
import { internationalizationContext } from "../../data/context";
@@ -67,8 +67,6 @@ class HaInputMulti extends LitElement {
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
@query("ha-input[data-last]") private _lastInput?: HaInput;
protected render() {
return html`
<ha-sortable
@@ -165,7 +163,10 @@ class HaInputMulti extends LitElement {
const items = [...this._items, ""];
this._fireChanged(items);
await this.updateComplete;
this._lastInput?.focus();
const field = this.shadowRoot?.querySelector(`ha-input[data-last]`) as
| HaInput
| undefined;
field?.focus();
}
private async _editItem(ev: Event) {
+2 -4
View File
@@ -1,6 +1,6 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html } from "lit";
import { customElement, property, query } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import "../ha-ripple";
import { HaListItemBase } from "./ha-list-item-base";
@@ -34,10 +34,8 @@ export class HaListItemButton extends HaListItemBase {
@property({ type: String }) public download?: string;
@query("#item") private _item?: HTMLElement;
public override activate(): void {
this._item?.click();
this.renderRoot.querySelector<HTMLElement>("#item")?.click();
}
protected _renderBase(inner: TemplateResult): TemplateResult {
+8 -4
View File
@@ -130,6 +130,10 @@ export class HaRowItem extends LitElement {
color: var(--primary-text-color);
font-size: var(--ha-font-size-m);
line-height: var(--ha-line-height-normal);
--ha-row-item-padding-block: var(--ha-space-3);
--ha-row-item-padding-inline: var(--ha-space-4);
--ha-row-item-gap: var(--ha-space-4);
--ha-row-item-min-height: 48px;
}
:host([disabled]) {
color: var(--disabled-text-color);
@@ -140,10 +144,10 @@ export class HaRowItem extends LitElement {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--ha-row-item-gap, var(--ha-space-4));
padding-block: var(--ha-row-item-padding-block, var(--ha-space-3));
padding-inline: var(--ha-row-item-padding-inline, var(--ha-space-4));
min-height: var(--ha-row-item-min-height, 48px);
gap: var(--ha-row-item-gap);
padding-block: var(--ha-row-item-padding-block);
padding-inline: var(--ha-row-item-padding-inline);
min-height: var(--ha-row-item-min-height);
box-sizing: border-box;
}
.content {
+4 -2
View File
@@ -292,12 +292,14 @@ export class HaListBase extends LitElement {
static styles: CSSResultGroup = css`
:host {
display: block;
--ha-list-gap: 0;
--ha-list-padding: 0;
}
.base {
display: flex;
flex-direction: column;
gap: var(--ha-list-gap, 0);
padding: var(--ha-list-padding, 0);
gap: var(--ha-list-gap);
padding: var(--ha-list-padding);
margin: 0;
list-style: none;
}
+3 -3
View File
@@ -121,15 +121,15 @@ export class HaListSelectable extends HaListBase {
public updateListItems() {
super.updateListItems();
this._syncItemSelectedState(true);
this._syncItemSelectedState();
}
private _sortedSelectedIndices(): number[] {
return [...this._selectedIndices!].sort((a, b) => a - b);
}
private _syncItemSelectedState(reset = false): void {
if (!this._selectedIndices || reset) {
private _syncItemSelectedState() {
if (!this._selectedIndices) {
this._selectedIndices = new Set<number>();
this.items.forEach((item, i) => {
const opt = item as HaListItemOption;
+6 -8
View File
@@ -12,7 +12,7 @@ import type {
} from "leaflet";
import type { PropertyValues } from "lit";
import { css, ReactiveElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { formatDateTime } from "../../common/datetime/format_date_time";
import {
formatTimeWeekday,
@@ -105,8 +105,6 @@ export class HaMap extends ReactiveElement {
@state() private _loaded = false;
@query("#map") private _mapElement?: HTMLElement;
public leafletMap?: Map;
private Leaflet?: LeafletModuleType;
@@ -237,11 +235,11 @@ export class HaMap extends ReactiveElement {
}
private _updateMapStyle(): void {
const map = this._mapElement!;
map.classList.toggle("clickable", this.clickable);
map.classList.toggle("dark", this._darkMode);
map.classList.toggle("forced-dark", this.themeMode === "dark");
map.classList.toggle("forced-light", this.themeMode === "light");
const map = this.renderRoot.querySelector("#map");
map!.classList.toggle("clickable", this.clickable);
map!.classList.toggle("dark", this._darkMode);
map!.classList.toggle("forced-dark", this.themeMode === "dark");
map!.classList.toggle("forced-light", this.themeMode === "light");
}
private _loading = false;
@@ -22,6 +22,8 @@ import "../../ha-adaptive-dialog";
import "../../ha-dialog-header";
import "../../ha-icon-button";
import "../../ha-icon-next";
import "../../ha-md-list";
import "../../ha-md-list-item";
import "../../ha-svg-icon";
import "../../list/ha-list-base";
import "../ha-target-picker-item-row";
@@ -28,6 +28,8 @@ import "../ha-domain-icon";
import { floorDefaultIconPath } from "../ha-floor-icon";
import "../ha-icon";
import "../ha-icon-button";
import "../ha-md-list";
import "../ha-md-list-item";
import "../ha-state-icon";
import "../ha-tooltip";
@@ -5,15 +5,19 @@ import { customElement, property } from "lit/decorators";
import "../ha-code-editor";
import "../ha-icon-button";
import type { TraceExtended } from "../../data/trace";
import type { HomeAssistant } from "../../types";
@customElement("ha-trace-blueprint-config")
export class HaTraceBlueprintConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trace!: TraceExtended;
protected render(): TemplateResult {
return html`
<ha-code-editor
.value=${dump(this.trace.blueprint_inputs || "").trimRight()}
.hass=${this.hass}
read-only
dir="ltr"
></ha-code-editor>
+4
View File
@@ -5,15 +5,19 @@ import { customElement, property } from "lit/decorators";
import "../ha-code-editor";
import "../ha-icon-button";
import type { TraceExtended } from "../../data/trace";
import type { HomeAssistant } from "../../types";
@customElement("ha-trace-config")
export class HaTraceConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trace!: TraceExtended;
protected render(): TemplateResult {
return html`
<ha-code-editor
.value=${dump(this.trace.config).trimRight()}
.hass=${this.hass}
read-only
dir="ltr"
></ha-code-editor>
@@ -271,6 +271,7 @@ export class HaTracePathDetails extends LitElement {
return config
? html`<ha-code-editor
.value=${dump(config).trimEnd()}
.hass=${this.hass}
read-only
dir="ltr"
></ha-code-editor>`
@@ -310,6 +311,7 @@ export class HaTracePathDetails extends LitElement {
: html`<ha-code-editor
read-only
dir="ltr"
.hass=${this.hass}
.value=${dump(trace.changed_variables).trimEnd()}
></ha-code-editor>`}
`
+7 -8
View File
@@ -20,7 +20,7 @@ import {
} from "@mdi/js";
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import type { Condition, Trigger } from "../../data/automation";
@@ -73,9 +73,6 @@ export class HatScriptGraph extends LitElement {
@property({ attribute: false }) public selected?: string;
@query("hat-graph-node[active], hat-graph-branch[active]")
private _activeNode?: HTMLElement;
public hass!: HomeAssistant;
public renderedNodes: Record<string, NodeInfo> = {};
@@ -670,10 +667,12 @@ export class HatScriptGraph extends LitElement {
// Scroll to active node when selection changes
if (changedProps.has("selected")) {
this._activeNode?.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
const activeNode = this.renderRoot.querySelector(
"hat-graph-node[active], hat-graph-branch[active]"
) as HTMLElement;
if (activeNode) {
activeNode.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
}
if (!changedProps.has("trace")) {
-9
View File
@@ -5,7 +5,6 @@ import type {
HomeAssistantApi,
HomeAssistantConfig,
HomeAssistantConnection,
HomeAssistantFormatters,
HomeAssistantInternationalization,
HomeAssistantRegistries,
HomeAssistantUI,
@@ -64,14 +63,6 @@ export const uiContext = createContext<HomeAssistantUI>("hassUi");
*/
export const configContext = createContext<HomeAssistantConfig>("hassConfig");
/**
* Entity formatting functions: `formatEntityState`, `formatEntityStateToParts`,
* `formatEntityAttributeValue`, `formatEntityAttributeValueToParts`,
* `formatEntityAttributeName`, and `formatEntityName`.
*/
export const formattersContext =
createContext<HomeAssistantFormatters>("hassFormatters");
/**
* Map of all entities in the entity registry, keyed by entity ID.
*/
-28
View File
@@ -3,7 +3,6 @@ import type {
HomeAssistantApi,
HomeAssistantConfig,
HomeAssistantConnection,
HomeAssistantFormatters,
HomeAssistantInternationalization,
HomeAssistantRegistries,
HomeAssistantUI,
@@ -157,32 +156,6 @@ const updateConfig = (
return value;
};
const updateFormatters = (
hass: HomeAssistant,
value?: HomeAssistantFormatters
): HomeAssistantFormatters => {
if (
!value ||
value.formatEntityState !== hass.formatEntityState ||
value.formatEntityStateToParts !== hass.formatEntityStateToParts ||
value.formatEntityAttributeValue !== hass.formatEntityAttributeValue ||
value.formatEntityAttributeValueToParts !==
hass.formatEntityAttributeValueToParts ||
value.formatEntityAttributeName !== hass.formatEntityAttributeName ||
value.formatEntityName !== hass.formatEntityName
) {
return {
formatEntityState: hass.formatEntityState,
formatEntityStateToParts: hass.formatEntityStateToParts,
formatEntityAttributeValue: hass.formatEntityAttributeValue,
formatEntityAttributeValueToParts: hass.formatEntityAttributeValueToParts,
formatEntityAttributeName: hass.formatEntityAttributeName,
formatEntityName: hass.formatEntityName,
};
}
return value;
};
export const updateHassGroups = {
registries: updateRegistries,
internationalization: updateInternationalization,
@@ -190,5 +163,4 @@ export const updateHassGroups = {
connection: updateConnection,
ui: updateUi,
config: updateConfig,
formatters: updateFormatters,
};
-4
View File
@@ -182,8 +182,6 @@ export interface GasSourceTypeEnergyPreference {
entity_energy_price: string | null;
number_energy_price: number | null;
unit_of_measurement?: string | null;
name?: string;
}
export interface WaterSourceTypeEnergyPreference {
@@ -202,8 +200,6 @@ export interface WaterSourceTypeEnergyPreference {
entity_energy_price: string | null;
number_energy_price: number | null;
unit_of_measurement?: string | null;
name?: string;
}
export type EnergySource =
+4 -7
View File
@@ -154,7 +154,7 @@ export const getRecorderInfo = (conn: Connection) =>
});
export const getStatisticIds = (
hass: Pick<HomeAssistant, "callWS">,
hass: HomeAssistant,
statistic_type?: "mean" | "sum"
) =>
hass.callWS<StatisticsMetaData[]>({
@@ -227,7 +227,7 @@ export const fetchStatistic = (
rolling_window: period.rolling_window,
});
export const validateStatistics = (hass: Pick<HomeAssistant, "callWS">) =>
export const validateStatistics = (hass: HomeAssistant) =>
hass.callWS<StatisticsValidationResults>({
type: "recorder/validate_statistics",
});
@@ -245,10 +245,7 @@ export const updateStatisticsMetadata = (
unit_class,
});
export const clearStatistics = (
hass: Pick<HomeAssistant, "callWS">,
statistic_ids: string[]
) =>
export const clearStatistics = (hass: HomeAssistant, statistic_ids: string[]) =>
hass.callWS<undefined>({
type: "recorder/clear_statistics",
statistic_ids,
@@ -372,5 +369,5 @@ export const getDisplayUnit = (
export const isExternalStatistic = (statisticsId: string): boolean =>
statisticsId.includes(":");
export const updateStatisticsIssues = (hass: Pick<HomeAssistant, "callWS">) =>
export const updateStatisticsIssues = (hass: HomeAssistant) =>
hass.callWS<undefined>({ type: "recorder/update_statistics_issues" });
@@ -2,11 +2,11 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-bottom-sheet";
import "../../components/ha-dialog";
import "../../components/ha-icon";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/ha-svg-icon";
import "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import "../../components/ha-dialog";
import type { HomeAssistant } from "../../types";
import type { HassDialog } from "../make-dialog-manager";
import type { ListItemsDialogParams } from "./show-list-items-dialog";
@@ -51,30 +51,41 @@ export class ListItemsDialog
const content = html`
<div class="container">
<ha-list-base>
<ha-md-list>
${this._params.items.map(
(item) => html`
<ha-list-item-button @click=${this._itemClicked} .item=${item}>
<ha-md-list-item
type="button"
@click=${this._itemClicked}
.item=${item}
>
${item.iconPath
? html`
<ha-svg-icon
.path=${item.iconPath}
slot="start"
class="item-icon"
></ha-svg-icon>
`
: item.icon
? html` <ha-icon icon=${item.icon} slot="start"></ha-icon> `
? html`
<ha-icon
icon=${item.icon}
slot="start"
class="item-icon"
></ha-icon>
`
: nothing}
<span slot="headline">${item.label}</span>
<span class="headline">${item.label}</span>
${item.description
? html`
<span slot="supporting-text">${item.description}</span>
<span class="supporting-text">${item.description}</span>
`
: nothing}
</ha-list-item-button>
</ha-md-list-item>
`
)}
</ha-list-base>
</ha-md-list>
</div>
`;
@@ -102,16 +113,12 @@ export class ListItemsDialog
}
static styles = css`
ha-dialog,
ha-bottom-sheet {
ha-dialog {
/* Place above other dialogs */
--dialog-z-index: 104;
--dialog-content-padding: 0;
--ha-row-item-padding-inline: var(--ha-space-6);
}
ha-bottom-sheet {
--ha-bottom-sheet-content-padding: var(--ha-space-4) 0 0;
--md-list-item-leading-space: 24px;
--md-list-item-trailing-space: 24px;
}
`;
}
@@ -120,6 +120,7 @@ class MoreInfoScript extends LitElement {
...(this.data ? { data: this.data } : {}),
...this._scriptData,
}}
.showAdvanced=${this.hass.userData?.showAdvanced}
.narrow=${this.narrow}
@value-changed=${this._scriptDataChanged}
></ha-service-control>
@@ -9,9 +9,10 @@ import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-faded";
import "../../../components/ha-markdown";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-switch";
import "../../../components/item/ha-row-item";
import "../../../components/progress/ha-progress-bar";
import type { BackupConfig } from "../../../data/backup";
import { fetchBackupConfig } from "../../../data/backup";
@@ -273,22 +274,24 @@ class MoreInfoUpdate extends LitElement {
<div class="footer">
${createBackupTexts
? html`
<ha-row-item>
<span slot="headline">${createBackupTexts.title}</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
.checked=${this._createBackup}
@change=${this._createBackupChanged}
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
</ha-row-item>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">${createBackupTexts.title}</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
.checked=${this._createBackup}
@change=${this._createBackupChanged}
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
</ha-md-list-item>
</ha-md-list>
`
: nothing}
<div class="actions">
@@ -481,9 +484,20 @@ class MoreInfoUpdate extends LitElement {
z-index: 10;
}
ha-row-item {
ha-md-list {
width: 100%;
--ha-row-item-padding-inline: var(--ha-space-6);
box-sizing: border-box;
margin-bottom: calc(var(--ha-space-4) * -1);
margin-top: calc(var(--ha-space-1) * -1);
--md-sys-color-surface: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
}
ha-md-list-item {
--md-list-item-leading-space: var(--ha-space-6);
--md-list-item-trailing-space: var(--ha-space-6);
}
.actions {
+13 -8
View File
@@ -2,9 +2,8 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-alert";
import "../../components/ha-icon";
import "../../components/ha-md-list-item";
import "../../components/ha-spinner";
import "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import type {
ExternalEntityAddToAction,
ExternalEntityAddToActions,
@@ -91,23 +90,24 @@ export class HaMoreInfoAddTo extends LitElement {
}
return html`
<ha-list-base>
${this._externalActions?.actions.map(
<div class="actions-list">
${this._externalActions.actions.map(
(action) => html`
<ha-list-item-button
<ha-md-list-item
type="button"
.disabled=${!action.enabled}
.action=${action}
@click=${this._actionSelected}
>
<ha-icon slot="start" .icon=${action.mdi_icon}></ha-icon>
<span slot="headline">${action.name}</span>
<span>${action.name}</span>
${action.details
? html`<span slot="supporting-text">${action.details}</span>`
: nothing}
</ha-list-item-button>
</ha-md-list-item>
`
)}
</ha-list-base>
</div>
`;
}
@@ -125,6 +125,11 @@ export class HaMoreInfoAddTo extends LitElement {
padding: var(--ha-space-8);
}
.actions-list {
display: flex;
flex-direction: column;
}
ha-icon {
display: flex;
align-items: center;
@@ -57,6 +57,7 @@ class HaMoreInfoDetails extends LitElement {
<div class="content">
${this.yamlMode
? html`<ha-yaml-editor
.hass=${this.hass}
.value=${yamlData}
read-only
auto-update
@@ -23,16 +23,12 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
@state() private _notifications: PersistentNotification[] = [];
@state() public _open = false;
@state() private _drawerOpen = false;
@state() private _open = false;
@query("ha-drawer") private _drawer?: HaDrawer;
private _unsubNotifications?: UnsubscribeFunc;
private _openAnimationFrame?: number;
connectedCallback() {
super.connectedCallback();
window.addEventListener("location-changed", this.closeDialog);
@@ -41,10 +37,6 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("location-changed", this.closeDialog);
if (this._openAnimationFrame !== undefined) {
cancelAnimationFrame(this._openAnimationFrame);
this._openAnimationFrame = undefined;
}
}
showDialog({ narrow }) {
@@ -59,21 +51,22 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
}
);
this.style.setProperty(
"--ha-sidebar-width",
"--mdc-drawer-width",
`min(100vw, calc(${narrow ? window.innerWidth + "px" : "500px"} + var(--safe-area-inset-left, 0px)))`
);
this._open = true;
}
closeDialog = () => {
if (this._drawerOpen && this._drawer) {
if (this._drawer) {
this._drawer.open = false;
this._drawerOpen = false;
return;
}
this._drawerOpen = false;
this._open = false;
this._finalizeClose();
if (this._unsubNotifications) {
this._unsubNotifications();
this._unsubNotifications = undefined;
}
this._notifications = [];
fireEvent(this, "dialog-closed", { dialog: this.localName });
};
public willUpdate(changedProps: PropertyValues<this>): void {
@@ -84,17 +77,6 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
}
}
protected updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
if (changedProps.has("_open") && this._open && !this._drawerOpen) {
this._openAnimationFrame = requestAnimationFrame(() => {
this._openAnimationFrame = undefined;
this._drawerOpen = true;
});
}
}
protected render() {
if (!this._open) {
return nothing;
@@ -122,8 +104,8 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
return html`
<ha-drawer
type="modal"
.open=${this._drawerOpen}
@hass-drawer-closed=${this._dialogClosed}
open
@MDCDrawer:closed=${this._dialogClosed}
.direction=${computeRTLDirection(this.hass)}
>
<ha-header-bar>
@@ -175,9 +157,7 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
private _dialogClosed(ev: Event) {
ev.stopPropagation();
this._drawerOpen = false;
this._open = false;
this._finalizeClose();
}
private _dismissAll() {
@@ -185,19 +165,6 @@ export class HuiNotificationDrawer extends KeyboardShortcutMixin(LitElement) {
this.closeDialog();
}
private _finalizeClose() {
if (this._openAnimationFrame !== undefined) {
cancelAnimationFrame(this._openAnimationFrame);
this._openAnimationFrame = undefined;
}
if (this._unsubNotifications) {
this._unsubNotifications();
this._unsubNotifications = undefined;
}
this._notifications = [];
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected supportedSingleKeyShortcuts(): SupportedShortcuts {
return {
Escape: () => this.closeDialog(),
+28 -21
View File
@@ -10,15 +10,14 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/animation/ha-fade-in";
import "../../components/ha-adaptive-dialog";
import "../../components/ha-alert";
import "../../components/ha-expansion-panel";
import "../../components/animation/ha-fade-in";
import "../../components/ha-icon-next";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/ha-spinner";
import "../../components/item/ha-list-item-button";
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import "../../components/progress/ha-progress-bar";
import { fetchBackupInfo } from "../../data/backup";
import type { BackupManagerState } from "../../data/backup_manager";
@@ -131,8 +130,9 @@ class DialogRestart extends LitElement {
</div>
`
: html`
<ha-list-base dialogInitialFocus>
<ha-list-item-button
<ha-md-list dialogInitialFocus>
<ha-md-list-item
type="button"
@click=${this._reload}
.disabled=${this._loadingBackupInfo}
>
@@ -148,8 +148,9 @@ class DialogRestart extends LitElement {
<ha-svg-icon .path=${mdiAutoFix}></ha-svg-icon>
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button
</ha-md-list-item>
<ha-md-list-item
type="button"
.action=${"restart"}
@click=${this._handleAction}
.disabled=${this._loadingBackupInfo}
@@ -166,17 +167,18 @@ class DialogRestart extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-base>
</ha-md-list-item>
</ha-md-list>
<ha-expansion-panel
.header=${this.hass.localize(
"ui.dialogs.restart.advanced_options"
)}
>
<ha-list-base>
<ha-md-list>
${showRebootShutdown
? html`
<ha-list-item-button
<ha-md-list-item
type="button"
.action=${"reboot"}
@click=${this._handleAction}
.disabled=${this._loadingBackupInfo}
@@ -195,8 +197,9 @@ class DialogRestart extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button
</ha-md-list-item>
<ha-md-list-item
type="button"
.action=${"shutdown"}
@click=${this._handleAction}
.disabled=${this._loadingBackupInfo}
@@ -215,10 +218,11 @@ class DialogRestart extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-md-list-item>
`
: nothing}
<ha-list-item-button
<ha-md-list-item
type="button"
.action=${"restart-safe-mode"}
@click=${this._handleAction}
.disabled=${this._loadingBackupInfo}
@@ -240,8 +244,8 @@ class DialogRestart extends LitElement {
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-base>
</ha-md-list-item>
</ha-md-list>
</ha-expansion-panel>
`}
</div>
@@ -320,13 +324,16 @@ class DialogRestart extends LitElement {
}
};
private async _handleAction(ev: Event) {
private async _handleAction(ev) {
if (this._loadingBackupInfo) {
return;
}
this._loadingBackupInfo = true;
const action = (ev.currentTarget as HaListItemButton & { action: string })
.action as "restart" | "reboot" | "shutdown" | "restart-safe-mode";
const action = ev.currentTarget.action as
| "restart"
| "reboot"
| "shutdown"
| "restart-safe-mode";
const backupState = await this._loadBackupState();
@@ -1,9 +1,8 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/item/ha-list-item-button";
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import type { AssistSatelliteConfiguration } from "../../data/assist_satellite";
import { setWakeWords } from "../../data/assist_satellite";
import type { HomeAssistant } from "../../types";
@@ -36,28 +35,28 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
)}
</p>
</div>
<ha-list-base>
<ha-md-list>
${this.assistConfiguration!.available_wake_words.map(
(wakeWord) =>
html`<ha-list-item-button
html`<ha-md-list-item
interactive
type="button"
@click=${this._wakeWordPicked}
.value=${wakeWord.id}
>
${wakeWord.wake_word}
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>`
</ha-md-list-item>`
)}
</ha-list-base>`;
</ha-md-list>`;
}
private async _wakeWordPicked(ev: Event) {
private async _wakeWordPicked(ev) {
if (!this.assistEntityId) {
return;
}
const wakeWordId = (
ev.currentTarget as HaListItemButton & { value: string }
).value;
const wakeWordId = ev.currentTarget.value;
await setWakeWords(this.hass, this.assistEntityId, [wakeWordId]);
this._nextStep();
@@ -76,7 +75,7 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
.padding {
padding: 24px;
}
ha-list-base {
ha-md-list {
width: 100%;
text-align: initial;
margin-bottom: 24px;
@@ -363,6 +363,9 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
static styles = [
AssistantSetupStyles,
css`
ha-md-list-item {
text-align: initial;
}
ha-tts-voice-picker {
display: block;
}
+4 -2
View File
@@ -214,8 +214,10 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
if (
changedProperties.has("tabs") ||
(changedProperties.has("hass") &&
this.hass?.config.components !==
changedProperties.get("hass")?.config.components)
(this.hass?.config.components !==
changedProperties.get("hass")?.config.components ||
this.hass?.userData?.showAdvanced !==
changedProperties.get("hass")?.userData?.showAdvanced))
) {
this.showTabs =
this.tabs.filter((page) => canShowPage(this.hass, page)).length > 1;
+1
View File
@@ -33,6 +33,7 @@ export interface PageNavigation {
name?: string;
not_component?: string | string[];
core?: boolean;
advancedOnly?: boolean;
/** Hide from non-admin users in filtered navigation and quick bar. */
adminOnly?: boolean;
iconPath?: string;
+5 -5
View File
@@ -56,7 +56,7 @@ export class HomeAssistantMain extends LitElement {
.type=${sidebarNarrow ? "modal" : ""}
.open=${sidebarNarrow ? this._drawerOpen : false}
.direction=${computeRTLDirection(this.hass)}
@hass-drawer-closed=${this._drawerClosed}
@MDCDrawer:closed=${this._drawerClosed}
>
<ha-sidebar
.hass=${this.hass}
@@ -152,16 +152,16 @@ export class HomeAssistantMain extends LitElement {
color: var(--primary-text-color);
/* remove the grey tap highlights in iOS on the fullscreen touch targets */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
--ha-sidebar-width: calc(56px + var(--safe-area-inset-left, 0px));
--mdc-top-app-bar-width: calc(100% - var(--ha-sidebar-width));
--mdc-drawer-width: calc(56px + var(--safe-area-inset-left, 0px));
--mdc-top-app-bar-width: calc(100% - var(--mdc-drawer-width));
--safe-area-content-inset-left: 0px;
--safe-area-content-inset-right: var(--safe-area-inset-right);
}
:host([expanded]) {
--ha-sidebar-width: calc(256px + var(--safe-area-inset-left, 0px));
--mdc-drawer-width: calc(256px + var(--safe-area-inset-left, 0px));
}
:host([modal]) {
--ha-sidebar-width: unset;
--mdc-drawer-width: unset;
--mdc-top-app-bar-width: unset;
--safe-area-content-inset-left: var(--safe-area-inset-left);
}
+5 -4
View File
@@ -1,6 +1,6 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { LOCAL_TIME_ZONE } from "../common/datetime/resolve-time-zone";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
@@ -42,8 +42,6 @@ class OnboardingCoreConfig extends LitElement {
@state() private _skipCore = false;
@query("ha-country-picker") private _countryPicker?: HTMLElement;
protected render(): TemplateResult {
if (!this._location) {
return html`<onboarding-location
@@ -145,7 +143,10 @@ class OnboardingCoreConfig extends LitElement {
fireEvent(this, "onboarding-progress", { increase: 0.5 });
await this.updateComplete;
setTimeout(() => this._countryPicker!.focus(), 100);
setTimeout(
() => this.renderRoot.querySelector("ha-country-picker")!.focus(),
100
);
}
private async _save(ev) {
+1 -3
View File
@@ -64,8 +64,6 @@ class OnboardingLocation extends LitElement {
@query("ha-locations-editor", true) private map!: HaLocationsEditor;
@query("ha-input") private _input?: HTMLElement;
protected render(): TemplateResult {
const addressAttribution = this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.location_address",
@@ -203,7 +201,7 @@ class OnboardingLocation extends LitElement {
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
setTimeout(() => this._input!.focus(), 100);
setTimeout(() => this.renderRoot.querySelector("ha-input")!.focus(), 100);
this.addEventListener("keyup", (ev) => {
if (ev.key === "Enter") {
this._save(ev);
+14 -13
View File
@@ -5,9 +5,9 @@ import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-button";
import "../components/ha-icon-next";
import "../components/item/ha-list-item-button";
import "../components/list/ha-list-base";
import "../components/ha-icon-button-next";
import "../components/ha-md-list";
import "../components/ha-md-list-item";
import type { HomeAssistant } from "../types";
import { onBoardingStyles } from "./styles";
@@ -37,8 +37,8 @@ class OnboardingWelcome extends LitElement {
</div>
</div>
<ha-list-base>
<ha-list-item-button @click=${this._restoreBackupUpload}>
<ha-md-list>
<ha-md-list-item type="button" @click=${this._restoreBackupUpload}>
<div slot="headline">
${this.localize("ui.panel.page-onboarding.restore.upload_backup")}
</div>
@@ -47,18 +47,18 @@ class OnboardingWelcome extends LitElement {
"ui.panel.page-onboarding.restore.options.upload_description"
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
<ha-list-item-button @click=${this._restoreBackupCloud}>
<ha-icon-button-next slot="end"></ha-icon-button-next>
</ha-md-list-item>
<ha-md-list-item type="button" @click=${this._restoreBackupCloud}>
<div slot="headline">Home Assistant Cloud</div>
<div slot="supporting-text">
${this.localize(
"ui.panel.page-onboarding.restore.ha-cloud.description"
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
</ha-list-base>
<ha-icon-button-next slot="end"></ha-icon-button-next>
</ha-md-list-item>
</ha-md-list>
`;
}
@@ -123,10 +123,11 @@ class OnboardingWelcome extends LitElement {
padding: 0 var(--ha-space-4);
}
ha-list-base {
ha-md-list {
width: 100%;
padding-bottom: 0;
--ha-row-item-padding-inline: 0;
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
}
`,
];
@@ -8,8 +8,9 @@ import type { HaProgressButton } from "../../components/buttons/ha-progress-butt
import "../../components/ha-alert";
import "../../components/ha-button";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/input/ha-input";
import "../../components/item/ha-row-item";
import {
getPreferredAgentForDownload,
type BackupContentExtended,
@@ -91,30 +92,33 @@ class OnboardingRestoreBackupRestore extends LitElement {
</ha-alert>`
: nothing}
<ha-row-item>
<span slot="headline">
${this.localize(
"ui.panel.page-onboarding.restore.details.summary.created"
)}
</span>
<span slot="supporting-text">${formattedDate}</span>
</ha-row-item>
${onlyHomeAssistantBackup
? html`<ha-row-item>
<span slot="headline">
${this.localize(
"ui.panel.page-onboarding.restore.details.summary.content"
)}
</span>
<ha-backup-formfield-label
slot="supporting-text"
.version=${this.backup.homeassistant_version}
.label=${this.localize(
`ui.panel.page-onboarding.restore.data_picker.${this.backup.database_included ? "settings_and_history" : "settings"}`
)}
></ha-backup-formfield-label>
</ha-row-item>`
: nothing}
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${this.localize(
"ui.panel.page-onboarding.restore.details.summary.created"
)}
</span>
<span slot="supporting-text">${formattedDate}</span>
</ha-md-list-item>
${onlyHomeAssistantBackup
? html`<ha-md-list-item>
<span slot="headline">
${this.localize(
"ui.panel.page-onboarding.restore.details.summary.content"
)}
</span>
<ha-backup-formfield-label
slot="supporting-text"
.version=${this.backup.homeassistant_version}
.label=${this.localize(
`ui.panel.page-onboarding.restore.data_picker.${this.backup.database_included ? "settings_and_history" : "settings"}`
)}
></ha-backup-formfield-label>
</ha-md-list-item>`
: nothing}
</ha-md-list>
${!onlyHomeAssistantBackup
? html`<h2>
${this.localize("ui.panel.page-onboarding.restore.select_type")}
@@ -308,8 +312,26 @@ class OnboardingRestoreBackupRestore extends LitElement {
display: block;
margin-top: 16px;
}
ha-row-item {
--ha-row-item-padding-inline: 0;
ha-md-list {
background: none;
padding: 0;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-list-item-two-line-container-height: 64px;
--md-list-item-supporting-text-size: 1rem;
--md-list-item-label-text-size: 0.875rem;
--md-list-item-label-text-color: var(--secondary-text-color);
--md-list-item-supporting-text-color: var(--primary-text-color);
}
ha-md-list-item [slot="supporting-text"] {
display: flex;
align-items: center;
flex-direction: row;
gap: var(--ha-space-2);
line-height: var(--ha-line-height-condensed);
}
h2 {
font-size: var(--ha-font-size-xl);
+2 -4
View File
@@ -15,7 +15,7 @@ import {
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import memoize from "memoize-one";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import { resolveTimeZone } from "../../common/datetime/resolve-time-zone";
@@ -103,8 +103,6 @@ export class HAFullCalendar extends LitElement {
@state() private _activeView = this.initialView;
@query("style[data-fullcalendar]") private _fullCalendarStyle?: HTMLElement;
// @ts-ignore
private _resizeController = new ResizeController(this, {
callback: () => this.calendar?.updateSize(),
@@ -115,7 +113,7 @@ export class HAFullCalendar extends LitElement {
super.disconnectedCallback();
this.calendar?.destroy();
this.calendar = undefined;
this._fullCalendarStyle?.remove();
this.renderRoot.querySelector("style[data-fullcalendar]")?.remove();
}
connectedCallback(): void {
@@ -13,12 +13,13 @@ import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-faded";
import "../../../../../components/ha-markdown";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-switch";
import type { HaSwitch } from "../../../../../components/ha-switch";
import "../../../../../components/item/ha-row-item";
import type { HassioAddonDetails } from "../../../../../data/hassio/addon";
import {
fetchHassioAddonChangelog,
@@ -107,20 +108,25 @@ class SupervisorAppUpdateAvailableCard extends LitElement {
${createBackupTexts
? html`
<hr />
<ha-row-item>
<span slot="headline">
${createBackupTexts.title}
</span>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">
${createBackupTexts.title}
</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch slot="end" id="create-backup"></ha-switch>
</ha-row-item>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
id="create-backup"
></ha-switch>
</ha-md-list-item>
</ha-md-list>
`
: nothing}
`
@@ -267,10 +273,16 @@ class SupervisorAppUpdateAvailableCard extends LitElement {
margin: var(--ha-space-4) 0 0 0;
}
ha-row-item {
--ha-row-item-padding-inline: 0;
ha-md-list {
padding: 0;
margin-bottom: calc(-1 * var(--ha-space-4));
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
}
`,
];
}
@@ -1,7 +1,7 @@
import "@home-assistant/webawesome/dist/components/tag/tag";
import {
mdiCheckCircle,
mdiChip,
mdiCircleOffOutline,
mdiCursorDefaultClickOutline,
mdiDocker,
mdiExclamationThick,
@@ -17,10 +17,11 @@ import {
mdiNumeric6,
mdiNumeric7,
mdiNumeric8,
mdiPlayCircle,
mdiPound,
mdiShield,
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -30,7 +31,6 @@ import { atLeastVersion } from "../../../../../common/config/version";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { navigate } from "../../../../../common/navigate";
import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter";
import type { LocalizeKeys } from "../../../../../common/translations/localize";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/chips/ha-assist-chip";
import "../../../../../components/chips/ha-chip-set";
@@ -79,9 +79,9 @@ import { bytesToString } from "../../../../../util/bytes-to-string";
import { getAppDisplayName } from "../../common/app";
import "../../components/supervisor-apps-card-content";
import "../components/supervisor-app-metric";
import "../components/supervisor-app-update-available-card";
import { extractChangelog } from "../util/supervisor-app";
import "./supervisor-app-system-managed";
import "../components/supervisor-app-update-available-card";
const STAGE_ICON = {
stable: mdiCheckCircle,
@@ -203,10 +203,28 @@ class SupervisorAppInfo extends LitElement {
: nothing}
<div class="addon-version light-color">
${this.addon.version
? html`<supervisor-apps-state
.state=${this.addon.state}
></supervisor-apps-state>`
: this.addon.version_latest}
? html`
${this._computeIsRunning
? html`
<ha-svg-icon
.title=${this.hass.localize(
"ui.panel.config.apps.dashboard.app_running"
)}
class="running"
.path=${mdiPlayCircle}
></ha-svg-icon>
`
: html`
<ha-svg-icon
.title=${this.hass.localize(
"ui.panel.config.apps.dashboard.app_stopped"
)}
class="stopped"
.path=${mdiCircleOffOutline}
></ha-svg-icon>
`}
`
: html` ${this.addon.version_latest} `}
</div>
</div>
<div class="description light-color">
@@ -819,7 +837,7 @@ class SupervisorAppInfo extends LitElement {
const id = ev.currentTarget.id as AddonCapability;
showAlertDialog(this, {
title: this.hass.localize(
`ui.panel.config.apps.dashboard.capability.${id}.title` as LocalizeKeys
`ui.panel.config.apps.dashboard.capability.${id}.title`
),
text: this.hass.localize(
`ui.panel.config.apps.dashboard.capability.${id}.description`
@@ -1,20 +1,11 @@
import "@home-assistant/webawesome/dist/components/tag/tag";
import { mdiHelpCircleOutline } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-svg-icon";
import type { AddonStage, AddonState } from "../../../../data/hassio/addon";
import type { AddonStage } from "../../../../data/hassio/addon";
import type { HomeAssistant } from "../../../../types";
import { getAppDisplayName } from "../common/app";
import "./supervisor-apps-state";
import "./supervisor-apps-tag";
export interface AppTag {
label: string;
variant: "brand" | "success" | "warning" | "danger" | "neutral";
iconPath?: string;
}
@customElement("supervisor-apps-card-content")
class SupervisorAppsCardContent extends LitElement {
@@ -25,13 +16,13 @@ class SupervisorAppsCardContent extends LitElement {
@property() public stage: AddonStage = "stable";
@property() public state: AddonState = null;
@property() public description?: string;
@property({ type: Boolean }) public available = true;
@property({ attribute: false }) public tags?: AppTag[];
@property({ attribute: false }) public showTopbar = false;
@property({ attribute: false }) public topbarClass?: string;
@property({ attribute: false }) public iconTitle?: string;
@@ -42,87 +33,78 @@ class SupervisorAppsCardContent extends LitElement {
@property({ attribute: false }) public iconImage?: string;
protected render(): TemplateResult {
return html`
<div class="app">
<div class="icon-wrapper">
${this.iconImage
? html`
<img
class="icon-image"
src=${this.iconImage}
.title=${this.iconTitle}
alt=${this.iconTitle ?? ""}
/>
`
: html`
<ha-svg-icon
class="app-icon"
.path=${this.icon}
.title=${this.iconTitle}
></ha-svg-icon>
`}
</div>
<div>
<div class="title-row">
<div class="title">
${getAppDisplayName(this.title, this.stage)}
</div>
</div>
<div class="addition">
${this.description}
${
/* treat as available when undefined */
this.available === false ? " (Not available)" : ""
}
</div>
</div>
</div>
${this.tags?.length || this.state
? html`
<div class="footer">
<supervisor-apps-state
.state=${this.state || "unknown"}
></supervisor-apps-state>
const stageLabel =
this.stage !== "stable"
? this.hass.localize(
`ui.panel.config.apps.dashboard.capability.stages.${this.stage}`
)
: undefined;
${this.tags?.length
? html`<div class="tags">
${this.tags.map(
(tag) =>
html`<supervisor-apps-tag
.variant=${tag.variant}
.iconPath=${tag.iconPath}
.label=${tag.label}
></supervisor-apps-tag>`
)}
</div>`
: nothing}
return html`
${this.showTopbar
? html` <div class="topbar ${this.topbarClass}"></div> `
: ""}
${this.iconImage
? html`
<div class="icon_image ${this.iconClass}">
<img
src=${this.iconImage}
.title=${this.iconTitle}
alt=${this.iconTitle ?? ""}
/>
<div></div>
</div>
`
: nothing}
: html`
<ha-svg-icon
class=${this.iconClass!}
.path=${this.icon}
.title=${this.iconTitle}
></ha-svg-icon>
`}
<div>
<div class="title-row">
<div class="title">${getAppDisplayName(this.title, this.stage)}</div>
${stageLabel
? html` <span class="stage ${this.stage}"> ${stageLabel} </span> `
: nothing}
</div>
<div class="addition">
${this.description}
${
/* treat as available when undefined */
this.available === false ? " (Not available)" : ""
}
</div>
</div>
`;
}
static styles = css`
.app {
margin-bottom: var(--ha-space-2);
gap: var(--ha-space-4);
display: flex;
:host {
direction: ltr;
}
.icon-wrapper {
position: relative;
margin-top: var(--ha-space-1);
width: 40px;
height: 40px;
flex-shrink: 0;
}
.app-icon {
ha-svg-icon {
margin-right: var(--ha-space-6);
margin-left: var(--ha-space-2);
margin-top: var(--ha-space-2);
margin-top: var(--ha-space-3);
float: left;
color: var(--secondary-text-color);
}
.icon-image {
max-height: 40px;
max-width: 40px;
ha-svg-icon.update {
color: var(--warning-color);
}
ha-svg-icon.running,
ha-svg-icon.installed {
color: var(--success-color);
}
ha-svg-icon.hassupdate,
ha-svg-icon.backup {
color: var(--state-icon-color);
}
ha-svg-icon.not_available {
color: var(--error-color);
}
.title {
flex: 1;
@@ -138,6 +120,22 @@ class SupervisorAppsCardContent extends LitElement {
gap: var(--ha-space-2);
min-width: 0;
}
.stage {
flex: none;
border-radius: 999px;
font-size: 12px;
line-height: 1;
padding: 4px 8px;
white-space: nowrap;
}
.stage.experimental {
color: var(--warning-color);
background-color: rgba(var(--rgb-warning-color), 0.12);
}
.stage.deprecated {
color: var(--error-color);
background-color: rgba(var(--rgb-error-color), 0.12);
}
.addition {
color: var(--secondary-text-color);
margin-top: var(--ha-space-1);
@@ -146,18 +144,43 @@ class SupervisorAppsCardContent extends LitElement {
height: 2.4em;
line-height: var(--ha-line-height-condensed);
}
.footer {
border-top: var(--ha-border-width-sm) solid
var(--ha-color-border-neutral-quiet);
padding-top: var(--ha-space-2);
display: flex;
gap: var(--ha-space-2);
flex-wrap: wrap;
justify-content: space-between;
.icon_image img {
max-height: 40px;
max-width: 40px;
margin-top: var(--ha-space-1);
margin-right: var(--ha-space-4);
float: left;
}
.tags {
display: flex;
gap: var(--ha-space-2);
.icon_image.stopped,
.icon_image.not_available {
filter: grayscale(1);
}
.dot {
position: absolute;
background-color: var(--warning-color);
width: 12px;
height: 12px;
top: var(--ha-space-2);
right: var(--ha-space-2);
border-radius: var(--ha-border-radius-circle);
}
.topbar {
position: absolute;
width: 100%;
height: 2px;
top: 0;
left: 0;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.topbar.installed {
background-color: var(--primary-color);
}
.topbar.update {
background-color: var(--accent-color);
}
.topbar.unavailable {
background-color: var(--error-color);
}
`;
}
@@ -1,79 +0,0 @@
import { consume, type ContextType } from "@lit/context";
import { mdiHelpCircle } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../components/ha-svg-icon";
import { internationalizationContext } from "../../../../data/context";
import type { AddonState } from "../../../../data/hassio/addon";
@customElement("supervisor-apps-state")
class SupervisorAppsState extends LitElement {
@property() public state: Exclude<AddonState, null> = "unknown";
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
protected render(): TemplateResult {
return html`
${this.state === "unknown"
? html`<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>`
: html` <div class="dot state-${this.state}"></div> `}
<span
>${this._i18n.localize(
`ui.panel.config.apps.dashboard.capability.state.${this.state}`
)}</span
>
`;
}
static styles = css`
:host {
display: inline-flex;
align-items: center;
gap: var(--ha-space-2);
color: var(--ha-color-text-secondary);
font-size: var(--ha-font-size-m);
}
.dot {
width: 8px;
height: 8px;
border-radius: var(--ha-border-radius-circle);
background-color: var(--ha-color-on-neutral-normal);
flex-shrink: 0;
}
.dot.state-started {
background-color: var(--ha-color-green-80);
animation: state-dot-pulse 1.8s infinite;
}
.dot.state-startup {
background-color: var(--ha-color-on-warning-normal);
}
.dot.state-error {
background-color: var(--ha-color-on-danger-normal);
}
ha-svg-icon {
--mdc-icon-size: 20px;
}
@keyframes state-dot-pulse {
0% {
box-shadow: 0 0 0 0 rgba(var(--rgb-success-color), 0.6);
}
100% {
box-shadow: 0 0 0 6px rgba(var(--rgb-success-color), 0);
}
}
@media (prefers-reduced-motion) {
.dot.state-started {
animation: none;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-apps-state": SupervisorAppsState;
}
}
@@ -1,64 +0,0 @@
import "@home-assistant/webawesome/dist/components/tag/tag";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-svg-icon";
@customElement("supervisor-apps-tag")
class SupervisorAppsTag extends LitElement {
@property() public variant:
| "brand"
| "success"
| "warning"
| "danger"
| "neutral" = "neutral";
@property({ attribute: "icon-path" }) public iconPath?: string;
@property() public label!: string;
protected render(): TemplateResult {
return html`<wa-tag .variant=${this.variant}>
${this.iconPath
? html`<ha-svg-icon .path=${this.iconPath}></ha-svg-icon>`
: nothing}
${this.label}
</wa-tag>`;
}
static styles = css`
wa-tag {
font-size: var(--ha-font-size-xs);
border-radius: var(--ha-border-radius-pill);
height: 20px;
border: none;
padding-inline: var(--ha-space-1) var(--ha-space-2);
}
wa-tag ha-svg-icon {
--mdc-icon-size: 16px;
width: 16px;
height: 16px;
}
wa-tag[variant="success"] {
color: var(--ha-color-on-success-normal);
}
wa-tag[variant="warning"] {
color: var(--ha-color-on-warning-normal);
}
wa-tag[variant="danger"] {
color: var(--ha-color-on-error-normal);
}
wa-tag[variant="neutral"] {
color: var(--ha-color-on-neutral-normal);
}
wa-tag[variant="brand"] {
color: var(--ha-color-on-primary-normal);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-apps-tag": SupervisorAppsTag;
}
}
@@ -1,8 +1,5 @@
import {
mdiAlertDecagramOutline,
mdiArrowUpBoldCircle,
mdiArrowUpBoldCircleOutline,
mdiFlask,
mdiPuzzle,
mdiRefresh,
mdiStorePlus,
@@ -32,9 +29,7 @@ import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../types";
import { getAppDisplayName } from "./common/app";
import "./components/supervisor-apps-card-content";
import type { AppTag } from "./components/supervisor-apps-card-content";
import { supervisorAppsStyle } from "./resources/supervisor-apps-style";
@customElement("ha-config-apps-installed")
@@ -101,59 +96,65 @@ export class HaConfigAppsInstalled extends LitElement {
</ha-input-search>
</div>
<div class="content">
${addons.length === 0
? html`
<ha-card outlined>
<div class="card-content">
<button class="link" @click=${this._openStore}>
${this.hass.localize(
"ui.panel.config.apps.installed.no_apps"
)}
</button>
</div>
</ha-card>
`
: addons.map(
(addon) => html`
<ha-card
role="button"
tabindex="0"
outlined
.addon=${addon}
@click=${this._addonTapped}
aria-label=${getAppDisplayName(addon.name, addon.stage)}
>
<div class="card-group">
${addons.length === 0
? html`
<ha-card outlined>
<div class="card-content">
<supervisor-apps-card-content
.hass=${this.hass}
.title=${addon.name}
.stage=${addon.stage}
.description=${addon.description}
available
.tags=${this._getAppTags(addon)}
.state=${addon.state}
.icon=${addon.update_available
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? this.hass.localize(
"ui.panel.config.apps.installed.app_stopped"
)
: addon.update_available
? this.hass.localize(
"ui.panel.config.apps.installed.app_update_available"
)
: this.hass.localize(
"ui.panel.config.apps.installed.app_running"
)}
.iconImage=${addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
></supervisor-apps-card-content>
<button class="link" @click=${this._openStore}>
${this.hass.localize(
"ui.panel.config.apps.installed.no_apps"
)}
</button>
</div>
</ha-card>
`
)}
: addons.map(
(addon) => html`
<ha-card
outlined
.addon=${addon}
@click=${this._addonTapped}
>
<div class="card-content">
<supervisor-apps-card-content
.hass=${this.hass}
.title=${addon.name}
.stage=${addon.stage}
.description=${addon.description}
available
.showTopbar=${addon.update_available}
topbarClass="update"
.icon=${addon.update_available
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? this.hass.localize(
"ui.panel.config.apps.installed.app_stopped"
)
: addon.update_available
? this.hass.localize(
"ui.panel.config.apps.installed.app_update_available"
)
: this.hass.localize(
"ui.panel.config.apps.installed.app_running"
)}
.iconClass=${addon.update_available
? addon.state === "started"
? "update"
: "update stopped"
: addon.state === "started"
? "running"
: "stopped"}
.iconImage=${addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
></supervisor-apps-card-content>
</div>
</ha-card>
`
)}
</div>
</div>
<ha-button size="large" href="/config/apps/available">
@@ -216,32 +217,6 @@ export class HaConfigAppsInstalled extends LitElement {
navigate("/config/apps/available");
}
private _getAppTags(addon: HassioAddonInfo): AppTag[] {
const labels: AppTag[] = [];
if (addon.update_available) {
labels.push({
label: this.hass.localize(
`ui.panel.config.apps.state.update_available`
),
variant: "brand",
iconPath: mdiArrowUpBoldCircleOutline,
});
}
if (addon.stage !== "stable") {
labels.push({
label: this.hass.localize(
`ui.panel.config.apps.dashboard.capability.stages.${addon.stage}`
),
variant: addon.stage === "experimental" ? "warning" : "danger",
iconPath:
addon.stage === "experimental" ? mdiFlask : mdiAlertDecagramOutline,
});
}
return labels;
}
static styles: CSSResultGroup = [
supervisorAppsStyle,
css`
@@ -254,10 +229,7 @@ export class HaConfigAppsInstalled extends LitElement {
ha-card {
cursor: pointer;
overflow: hidden;
}
ha-card:hover {
background-color: var(--ha-color-fill-neutral-quiet-resting);
direction: ltr;
}
.search {
@@ -275,13 +247,10 @@ export class HaConfigAppsInstalled extends LitElement {
.content {
padding: var(--ha-space-4);
margin-bottom: var(--ha-space-18);
gap: var(--ha-space-4);
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(336px, 100%), 1fr));
}
.card-content {
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
padding: var(--ha-space-4);
}
button.link {
@@ -71,6 +71,7 @@ export default class HaAutomationActionEditor extends LitElement {
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.action}
@value-changed=${this._onYamlChange}
.readOnly=${this.disabled}
@@ -54,6 +54,7 @@ export class HaEventAction extends LitElement implements ActionElement {
@change=${this._eventChanged}
></ha-input>
<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.event.event_data"
)}
@@ -85,6 +85,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
.hass=${this.hass}
.value=${this._action}
.disabled=${this.disabled}
.showAdvanced=${this.hass.userData?.showAdvanced}
.hidePicker=${!!this._action.metadata}
@value-changed=${this._actionChanged}
></ha-service-control>
@@ -47,6 +47,8 @@ import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-button-prev";
import "../../../components/ha-icon-next";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
import "../../../components/ha-section-title";
import "../../../components/ha-service-icon";
@@ -54,8 +56,6 @@ import "../../../components/ha-tooltip";
import { TRIGGER_ICONS } from "../../../components/ha-trigger-icon";
import "../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../components/input/ha-input-search";
import "../../../components/item/ha-list-item-button";
import "../../../components/list/ha-list-base";
import {
ACTION_BUILDING_BLOCKS_GROUP,
ACTION_COLLECTIONS,
@@ -731,7 +731,7 @@ class DialogAddAutomationElement
.manifests=${this._manifests}
></ha-automation-add-from-target>`
: html`
<ha-list-base
<ha-md-list
class=${classMap({
groups: true,
hidden: hideCollections,
@@ -739,7 +739,9 @@ class DialogAddAutomationElement
})}
>
${this._params!.clipboardItem
? html`<ha-list-item-button
? html`<ha-md-list-item
interactive
type="button"
class="paste"
@click=${this._paste}
>
@@ -783,7 +785,7 @@ class DialogAddAutomationElement
slot="end"
.path=${mdiPlus}
></ha-svg-icon>
</ha-list-item-button>
</ha-md-list-item>
<wa-divider></wa-divider>`
: nothing}
${collections.map(
@@ -797,7 +799,9 @@ class DialogAddAutomationElement
collection.groups,
(item) => item.key,
(item) => html`
<ha-list-item-button
<ha-md-list-item
interactive
type="button"
.value=${item.key}
.index=${collection.collectionIndex}
@click=${this._groupSelected}
@@ -817,12 +821,12 @@ class DialogAddAutomationElement
${this._narrow
? html`<ha-icon-next slot="end"></ha-icon-next>`
: nothing}
</ha-list-item-button>
</ha-md-list-item>
`
)}
`
)}
</ha-list-base>
</ha-md-list>
`}
${!this._filter
? html`
@@ -2387,14 +2391,8 @@ class DialogAddAutomationElement
gap: var(--ha-space-3);
}
ha-list-item-button {
--ha-row-item-padding-block: var(--ha-space-1);
--ha-row-item-padding-inline: var(--ha-space-3);
--ha-row-item-min-height: 40px;
}
ha-list-item-button::part(start),
ha-list-item-button::part(end) {
color: var(--ha-color-on-neutral-quiet);
ha-md-list {
padding: 0;
}
ha-automation-add-from-target,
@@ -23,12 +23,11 @@ import { stringCompare } from "../../../../common/string/compare";
import "../../../../components/ha-floor-icon";
import "../../../../components/ha-icon";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-section-title";
import "../../../../components/ha-state-icon";
import "../../../../components/ha-svg-icon";
import "../../../../components/item/ha-list-item-button";
import "../../../../components/item/ha-row-item";
import "../../../../components/list/ha-list-base";
import {
getAreaDeviceLookup,
getAreaEntityLookup,
@@ -329,13 +328,15 @@ export default class HaAutomationAddFromTarget extends LitElement {
)}</ha-section-title
>
${emptyFloors
? html`<ha-row-item>
<div slot="headline">
${this._i18n.localize("ui.components.area-picker.no_areas")}
</div>
</ha-row-item>`
? html`<ha-md-list>
<ha-md-list-item type="text">
<div slot="headline">
${this._i18n.localize("ui.components.area-picker.no_areas")}
</div>
</ha-md-list-item>
</ha-md-list>`
: html`${narrow
? html`<ha-list-base>${floorAreas}</ha-list-base>`
? html`<ha-md-list>${floorAreas}</ha-md-list>`
: html`<wa-tree
@wa-selection-change=${this._handleSelectionChange}
>${floorAreas}</wa-tree
@@ -369,10 +370,12 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.components.label-picker.labels"
)}</ha-section-title
>
<ha-list-base>
<ha-md-list>
${labels.map(
(label) =>
html`<ha-list-item-button
html`<ha-md-list-item
interactive
type="button"
.target=${label.id}
@click=${this._selectItem}
class=${this._getSelectedTargetId(value) === label.id
@@ -390,9 +393,9 @@ export default class HaAutomationAddFromTarget extends LitElement {
${narrow
? html`<ha-icon-next slot="end"></ha-icon-next> `
: nothing}
</ha-list-item-button>`
</ha-md-list-item>`
)}
</ha-list-base>`;
</ha-md-list>`;
}
);
@@ -511,7 +514,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.panel.config.automation.editor.unassigned"
)}</ha-section-title
>${narrow
? html`<ha-list-base>${items}</ha-list-base>`
? html`<ha-md-list>${items}</ha-md-list>`
: html`<wa-tree @wa-selection-change=${this._handleSelectionChange}>
${items}
</wa-tree>`} `;
@@ -565,7 +568,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.components.target-picker.type.areas"
)}</ha-section-title
>
<ha-list-base>${renderedAreas}</ha-list-base>`;
<ha-md-list>${renderedAreas}</ha-md-list>`;
}
return renderedAreas;
@@ -614,7 +617,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.components.target-picker.type.devices"
)}</ha-section-title
>
<ha-list-base>${renderedDevices}</ha-list-base>`;
<ha-md-list>${renderedDevices}</ha-md-list>`;
}
return renderedDevices;
@@ -661,7 +664,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.components.target-picker.type.devices"
)}</ha-section-title
>
<ha-list-base>${renderedDomains}</ha-list-base>`;
<ha-md-list> ${renderedDomains} </ha-md-list>`;
}
return renderedDomains;
@@ -716,7 +719,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
"ui.components.target-picker.type.entities"
)}</ha-section-title
>
<ha-list-base>${renderedEntites}</ha-list-base>`;
<ha-md-list>${renderedEntites}</ha-md-list>`;
}
return renderedEntites;
@@ -781,14 +784,17 @@ export default class HaAutomationAddFromTarget extends LitElement {
children?: TemplateResult | TemplateResult[] | typeof nothing
) {
if (this.narrow) {
return html`<ha-list-item-button
return html`<ha-md-list-item
interactive
type="button"
.target=${target}
@click=${this._selectItem}
.title=${label}
>
${icon?.("start")}
<div slot="headline">${label}</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>`;
</ha-md-list-item>`;
}
return html`
@@ -1185,7 +1191,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
await this.updateComplete;
if (type === "label") {
this.shadowRoot!.querySelector(
"ha-list-item-button.selected"
"ha-md-list-item.selected"
)?.scrollIntoView({
block: "center",
});
@@ -1410,9 +1416,9 @@ export default class HaAutomationAddFromTarget extends LitElement {
font-weight: var(--ha-font-weight-medium);
overflow: hidden;
}
ha-list-item-button::part(label) {
font-weight: var(--ha-font-weight-medium);
font-family: var(--ha-font-family-heading);
ha-md-list-item {
--md-list-item-label-text-weight: var(--ha-font-weight-medium);
--md-list-item-label-text-font: var(--ha-font-family-heading);
}
.item-label {
@@ -1456,33 +1462,29 @@ export default class HaAutomationAddFromTarget extends LitElement {
background-color: yellow;
}
ha-list-base {
--ha-row-item-padding-inline: var(--ha-space-3);
--ha-row-item-padding-block: var(--ha-space-1);
--ha-row-item-min-height: 40px;
ha-md-list {
padding: 0;
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--md-list-item-leading-space);
--md-list-item-bottom-space: var(--ha-space-1);
--md-list-item-top-space: var(--md-list-item-bottom-space);
--md-list-item-supporting-text-font: var(--ha-font-size-s);
--md-list-item-one-line-container-height: var(--ha-space-10);
}
ha-list-item-button::part(end) {
color: var(--ha-color-on-neutral-quiet);
}
ha-list-item-button.selected {
ha-md-list-item.selected {
background-color: var(--ha-color-fill-primary-normal-active);
--md-list-item-label-text-color: var(--ha-color-on-primary-normal);
--icon-primary-color: var(--ha-color-on-primary-normal);
}
ha-list-item-button.selected::part(headline) {
color: var(--ha-color-on-primary-normal);
}
wa-tree-item[selected],
wa-tree-item[selected] > ha-svg-icon,
wa-tree-item[selected] > ha-icon,
wa-tree-item[selected] > ha-state-icon,
wa-tree-item[selected] > ha-floor-icon,
ha-list-item-button.selected ha-icon,
ha-list-item-button.selected ha-svg-icon {
ha-md-list-item.selected ha-icon,
ha-md-list-item.selected ha-svg-icon {
color: var(--ha-color-on-primary-normal);
}
@@ -12,15 +12,15 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import "../../../../components/item/ha-list-item-button";
import "../../../../components/list/ha-list-base";
import type { ConfigEntry } from "../../../../data/config_entries";
import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
import { haStyleScrollbar } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { AddAutomationElementListItem } from "../add-automation-element-dialog";
import { haStyleScrollbar } from "../../../../resources/styles";
import { getTargetIcon } from "../target/get_target_icon";
type Target = [string, string | undefined, string | undefined];
@@ -103,12 +103,17 @@ export class HaAutomationAddItems extends LitElement {
return html`
<div class="items-title">${title}</div>
<ha-list-base>
<ha-md-list>
${repeat(
items,
(item) => item.key,
(item) => html`
<ha-list-item-button .value=${item.key} @click=${this._selected}>
<ha-md-list-item
interactive
type="button"
.value=${item.key}
@click=${this._selected}
>
<div slot="headline" class=${this.target ? "item-headline" : ""}>
${item.name}${this._renderTarget(this.target)}
</div>
@@ -133,7 +138,6 @@ export class HaAutomationAddItems extends LitElement {
@click=${stopPropagation}
></ha-svg-icon>
<ha-tooltip
slot="end"
.for=${`description-tooltip-${item.key}`}
@wa-show=${stopPropagation}
@wa-hide=${stopPropagation}
@@ -147,10 +151,10 @@ export class HaAutomationAddItems extends LitElement {
class="plus"
.path=${mdiPlus}
></ha-svg-icon>
</ha-list-item-button>
</ha-md-list-item>
`
)}
</ha-list-base>
</ha-md-list>
`;
}
@@ -241,26 +245,24 @@ export class HaAutomationAddItems extends LitElement {
background-color: var(--ha-color-fill-danger-quiet-resting);
color: var(--ha-color-on-danger-normal);
}
.items ha-list-base {
--ha-row-item-padding-inline: var(--ha-space-3);
--ha-row-item-padding-block: var(--ha-space-2);
--ha-list-gap: var(--ha-space-3);
.items ha-md-list {
--md-list-item-two-line-container-height: var(--ha-space-12);
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--md-list-item-leading-space);
--md-list-item-bottom-space: var(--ha-space-2);
--md-list-item-top-space: var(--md-list-item-bottom-space);
--md-list-item-supporting-text-font: var(--ha-font-family-body);
--ha-md-list-item-gap: var(--ha-space-3);
gap: var(--ha-space-2);
padding: 0 var(--ha-space-4);
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-3));
}
.items ha-list-base ha-list-item-button {
.items ha-md-list ha-md-list-item {
border-radius: var(--ha-border-radius-lg);
border: 1px solid var(--ha-color-border-neutral-quiet);
overflow: hidden;
}
.items ha-list-base ha-list-item-button::part(start),
.items ha-list-base ha-list-item-button::part(end) {
color: var(--ha-color-on-neutral-quiet);
}
.items ha-list-base ha-list-item-button::part(end) {
gap: var(--ha-space-3);
.items ha-md-list {
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-3));
}
.items .item-headline {
@@ -8,6 +8,8 @@ import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-select-box";
import "../../../../components/input/ha-input";
@@ -79,6 +79,7 @@ export default class HaAutomationConditionEditor extends LitElement {
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.condition}
@value-changed=${this._onYamlChange}
.readOnly=${this.disabled}
@@ -186,6 +186,7 @@ export class HaPlatformCondition extends LitElement {
: nothing}
${shouldRenderDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.components.service-control.action_data"
)}
@@ -36,7 +36,6 @@ import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
import type {
AutomationConfig,
AutomationEntity,
@@ -122,8 +121,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
@query("manual-automation-editor")
private _manualEditor?: HaManualAutomationEditor;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
private _configSubscriptions: Record<
string,
(config?: AutomationConfig) => void
@@ -544,6 +541,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this.readOnly}
@value-changed=${this._yamlChanged}
@@ -830,7 +828,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
this.blueprintConfig = config;
this.config = newConfig;
if (this.mode === "yaml") {
this._yamlEditor?.setValue(this.config);
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
}
this.readOnly = true;
this.errors = undefined;
@@ -276,6 +276,7 @@ export class HaAutomationTrace extends LitElement {
: this._view === "automation_config"
? html`
<ha-trace-config
.hass=${this.hass}
.trace=${this._trace}
></ha-trace-config>
`
@@ -291,6 +292,7 @@ export class HaAutomationTrace extends LitElement {
: this._view === "blueprint"
? html`
<ha-trace-blueprint-config
.hass=${this.hass}
.trace=${this._trace}
></ha-trace-blueprint-config>
`
@@ -52,6 +52,7 @@ class DialogPasteReplace extends LitElement {
</p>
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._params?.pastedConfig}
read-only
></ha-yaml-editor>
+2 -2
View File
@@ -124,9 +124,9 @@ export const manualEditorStyles = css`
.has-sidebar {
--sidebar-width: min(
max(var(--sidebar-dynamic-width), ${SIDEBAR_MIN_WIDTH}px),
100vw - ${CONTENT_MIN_WIDTH}px - var(--ha-sidebar-width, 0px),
100vw - ${CONTENT_MIN_WIDTH}px - var(--mdc-drawer-width, 0px),
var(--ha-automation-editor-max-width) -
${CONTENT_MIN_WIDTH}px - var(--ha-sidebar-width, 0px)
${CONTENT_MIN_WIDTH}px - var(--mdc-drawer-width, 0px)
);
--sidebar-gap: var(--ha-space-4);
}
@@ -71,6 +71,7 @@ export default class HaAutomationTriggerEditor extends LitElement {
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.trigger}
.readOnly=${this.disabled}
@value-changed=${this._onYamlChange}
@@ -760,6 +760,7 @@ export default class HaAutomationTriggerRow extends LitElement {
<ha-yaml-editor
read-only
disable-fullscreen
.hass=${this.hass}
.defaultValue=${this._triggeredResult}
></ha-yaml-editor>
`,
@@ -34,6 +34,7 @@ export class HaEventTrigger extends LitElement implements TriggerElement {
@change=${this._valueChanged}
></ha-input>
<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.event.event_data"
)}
@@ -221,6 +221,7 @@ export class HaPlatformTrigger extends LitElement {
: nothing}
${shouldRenderDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.components.service-control.action_data"
)}
@@ -42,6 +42,7 @@ class SupervisorFormfieldLabel extends LitElement {
margin-right: 4px;
margin-inline-end: 4px;
margin-inline-start: initial;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-normal);
letter-spacing: 0.5px;
@@ -1,7 +1,7 @@
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
@@ -92,8 +92,6 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
@state() private _config?: BackupConfig;
@query("div") private _copyContainer?: HTMLElement;
public showDialog(params: BackupOnboardingDialogParams): void {
this._params = params;
@@ -480,7 +478,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
private async _copyKeyToClipboard() {
await copyToClipboard(
this._config!.create_backup.password!,
this._copyContainer!
this.renderRoot.querySelector("div")!
);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
@@ -1,7 +1,7 @@
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-button";
@@ -37,8 +37,6 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
@state() private _newEncryptionKey?: string;
@query("div") private _copyContainer?: HTMLElement;
public showDialog(params: ChangeBackupEncryptionKeyDialogParams): void {
this._params = params;
this._step = STEPS[0];
@@ -235,7 +233,10 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
}
private async _copyKeyToClipboard() {
await copyToClipboard(this._newEncryptionKey, this._copyContainer!);
await copyToClipboard(
this._newEncryptionKey,
this.renderRoot.querySelector("div")!
);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
@@ -245,7 +246,10 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
if (!this._params?.currentKey) {
return;
}
await copyToClipboard(this._params.currentKey, this._copyContainer!);
await copyToClipboard(
this._params.currentKey,
this.renderRoot.querySelector("div")!
);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
@@ -1,7 +1,7 @@
import { mdiContentCopy, mdiDownload } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-button";
@@ -36,8 +36,6 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
@state() private _newEncryptionKey?: string;
@query("div") private _copyContainer?: HTMLElement;
public showDialog(params: SetBackupEncryptionKeyDialogParams): void {
this._params = params;
this._step = STEPS[0];
@@ -180,7 +178,10 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
}
private async _copyKeyToClipboard() {
await copyToClipboard(this._newEncryptionKey, this._copyContainer!);
await copyToClipboard(
this._newEncryptionKey,
this.renderRoot.querySelector("div")!
);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});

Some files were not shown because too many files have changed in this diff Show More