diff --git a/pyproject.toml b/pyproject.toml index 421f20a4ab..7c48219044 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20230628.0" +version = "20230629.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index c41bde1263..998949b007 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -50,7 +50,7 @@ export const computeStateDisplay = ( entities: HomeAssistant["entities"], state?: string ): string => { - const entity = entities[stateObj.entity_id] as + const entity = entities?.[stateObj.entity_id] as | EntityRegistryDisplayEntry | undefined; diff --git a/src/components/entity/ha-statistic-picker.ts b/src/components/entity/ha-statistic-picker.ts index cfdb725c39..fc40f169ea 100644 --- a/src/components/entity/ha-statistic-picker.ts +++ b/src/components/entity/ha-statistic-picker.ts @@ -17,6 +17,16 @@ import "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box"; import "../ha-svg-icon"; import "./state-badge"; +import { + fuzzyFilterSort, + ScorableTextItem, +} from "../../common/string/filter/sequence-matching"; + +interface StatisticItem extends ScorableTextItem { + id: string; + name: string; + state?: HassEntity; +} @customElement("ha-statistic-picker") export class HaStatisticPicker extends LitElement { @@ -75,11 +85,11 @@ export class HaStatisticPicker extends LitElement { private _init = false; - private _rowRenderer: ComboBoxLitRenderer<{ - id: string; - name: string; - state?: HassEntity; - }> = (item) => html` + private _statistics: StatisticItem[] = []; + + private _rowRenderer: ComboBoxLitRenderer = ( + item + ) => html` ${item.state ? html`` : ""} @@ -105,7 +115,7 @@ export class HaStatisticPicker extends LitElement { includeUnitClass?: string | string[], includeDeviceClass?: string | string[], entitiesOnly?: boolean - ): Array<{ id: string; name: string; state?: HassEntity }> => { + ): StatisticItem[] => { if (!statisticIds.length) { return [ { @@ -113,6 +123,7 @@ export class HaStatisticPicker extends LitElement { name: this.hass.localize( "ui.components.statistic-picker.no_statistics" ), + strings: [], }, ]; } @@ -146,26 +157,28 @@ export class HaStatisticPicker extends LitElement { }); } - const output: Array<{ - id: string; - name: string; - state?: HassEntity; - }> = []; + const output: StatisticItem[] = []; statisticIds.forEach((meta) => { const entityState = this.hass.states[meta.statistic_id]; if (!entityState) { if (!entitiesOnly) { + const id = meta.statistic_id; + const name = getStatisticLabel(this.hass, meta.statistic_id, meta); output.push({ - id: meta.statistic_id, - name: getStatisticLabel(this.hass, meta.statistic_id, meta), + id, + name, + strings: [id, name], }); } return; } + const id = meta.statistic_id; + const name = getStatisticLabel(this.hass, meta.statistic_id, meta); output.push({ - id: meta.statistic_id, - name: getStatisticLabel(this.hass, meta.statistic_id, meta), + id, + name, state: entityState, + strings: [id, name], }); }); @@ -174,6 +187,7 @@ export class HaStatisticPicker extends LitElement { { id: "", name: this.hass.localize("ui.components.statistic-picker.no_match"), + strings: [], }, ]; } @@ -189,6 +203,7 @@ export class HaStatisticPicker extends LitElement { name: this.hass.localize( "ui.components.statistic-picker.missing_entity" ), + strings: [], }); return output; @@ -216,7 +231,7 @@ export class HaStatisticPicker extends LitElement { ) { this._init = true; if (this.hasUpdated) { - (this.comboBox as any).items = this._getStatistics( + this._statistics = this._getStatistics( this.statisticIds!, this.includeStatisticsUnitOfMeasurement, this.includeUnitClass, @@ -225,7 +240,7 @@ export class HaStatisticPicker extends LitElement { ); } else { this.updateComplete.then(() => { - (this.comboBox as any).items = this._getStatistics( + this._statistics = this._getStatistics( this.statisticIds!, this.includeStatisticsUnitOfMeasurement, this.includeUnitClass, @@ -248,11 +263,13 @@ export class HaStatisticPicker extends LitElement { .renderer=${this._rowRenderer} .disabled=${this.disabled} .allowCustomValue=${this.allowCustomEntity} + .filteredItems=${this._statistics} item-value-path="id" item-id-path="id" item-label-path="name" @opened-changed=${this._openedChanged} @value-changed=${this._statisticChanged} + @filter-changed=${this._filterChanged} > `; } @@ -281,6 +298,14 @@ export class HaStatisticPicker extends LitElement { this._opened = ev.detail.value; } + private _filterChanged(ev: CustomEvent): void { + const target = ev.target as HaComboBox; + const filterString = ev.detail.value.toLowerCase(); + target.filteredItems = filterString.length + ? fuzzyFilterSort(filterString, this._statistics) + : this._statistics; + } + private _setValue(value: string) { this.value = value; setTimeout(() => { diff --git a/src/components/ha-menu-button.ts b/src/components/ha-menu-button.ts index 2246dd136b..0e7fac5aa3 100644 --- a/src/components/ha-menu-button.ts +++ b/src/components/ha-menu-button.ts @@ -1,6 +1,6 @@ import { mdiMenu } from "@mdi/js"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; import { subscribeNotifications } from "../data/persistent_notification"; @@ -17,6 +17,8 @@ class HaMenuButton extends LitElement { @state() private _hasNotifications = false; + @state() private _show = false; + private _alwaysVisible = false; private _attachNotifOnConnect = false; @@ -40,7 +42,10 @@ class HaMenuButton extends LitElement { } } - protected render(): TemplateResult { + protected render() { + if (!this._show) { + return nothing; + } const hasNotifications = this._hasNotifications && (this.narrow || this.hass.dockedSidebar === "always_hidden"); @@ -66,8 +71,8 @@ class HaMenuButton extends LitElement { (Number((window.parent as any).frontendVersion) || 0) < 20190710; } - protected updated(changedProps) { - super.updated(changedProps); + protected willUpdate(changedProps) { + super.willUpdate(changedProps); if (!changedProps.has("narrow") && !changedProps.has("hass")) { return; @@ -85,11 +90,11 @@ class HaMenuButton extends LitElement { const showButton = this.narrow || this.hass.dockedSidebar === "always_hidden"; - if (oldShowButton === showButton) { + if (this.hasUpdated && oldShowButton === showButton) { return; } - this.style.display = showButton || this._alwaysVisible ? "initial" : "none"; + this._show = showButton || this._alwaysVisible; if (!showButton) { if (this._unsubNotifications) { diff --git a/src/dialogs/generic/dialog-box.ts b/src/dialogs/generic/dialog-box.ts index 5036664946..3b2ded7aad 100644 --- a/src/dialogs/generic/dialog-box.ts +++ b/src/dialogs/generic/dialog-box.ts @@ -81,6 +81,8 @@ class DialogBox extends LitElement { .type=${this._params.inputType ? this._params.inputType : "text"} + .min=${this._params.inputMin} + .max=${this._params.inputMax} > ` : ""} diff --git a/src/dialogs/generic/show-dialog-box.ts b/src/dialogs/generic/show-dialog-box.ts index 0b5de18606..7c95283aae 100644 --- a/src/dialogs/generic/show-dialog-box.ts +++ b/src/dialogs/generic/show-dialog-box.ts @@ -26,6 +26,8 @@ export interface PromptDialogParams extends BaseDialogBoxParams { placeholder?: string; confirm?: (out?: string) => void; cancel?: () => void; + inputMin?: number | string; + inputMax?: number | string; } export interface DialogBoxParams diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 30abaf7de3..5db6f06289 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -501,7 +501,7 @@ export class QuickBar extends LitElement { private async _generateCommandItems(): Promise { return [ - ...this._generateReloadCommands(), + ...(await this._generateReloadCommands()), ...this._generateServerControlCommands(), ...(await this._generateNavigationCommands()), ].sort((a, b) => @@ -513,17 +513,22 @@ export class QuickBar extends LitElement { ); } - private _generateReloadCommands(): CommandItem[] { + private async _generateReloadCommands(): Promise { // Get all domains that have a direct "reload" service const reloadableDomains = componentsWithService(this.hass, "reload"); + const localize = await this.hass.loadBackendTranslation( + "title", + reloadableDomains + ); + const commands = reloadableDomains.map((domain) => ({ primaryText: this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) || this.hass.localize( "ui.dialogs.quick-bar.commands.reload.reload", "domain", - domainToName(this.hass.localize, domain) + domainToName(localize, domain) ), action: () => this.hass.callService(domain, "reload"), iconPath: mdiReload, diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-conversation.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-conversation.ts index f5cc84d862..34a03c9ace 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-conversation.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-conversation.ts @@ -92,7 +92,11 @@ export class HaConversationTrigger private async _updateOption(ev: Event) { const index = (ev.target as any).index; - const command = [...this.trigger.command]; + const command = [ + ...(Array.isArray(this.trigger.command) + ? this.trigger.command + : [this.trigger.command]), + ]; command.splice(index, 1, (ev.target as HaTextField).value); fireEvent(this, "value-changed", { value: { ...this.trigger, command }, diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index b3dbecd72c..e55733aa3f 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -145,8 +145,15 @@ export class HaConfigDevicePage extends LitElement { ); private _integrations = memoizeOne( - (device: DeviceRegistryEntry, entries: ConfigEntry[]): ConfigEntry[] => - entries.filter((entry) => device.config_entries.includes(entry.entry_id)) + (device: DeviceRegistryEntry, entries: ConfigEntry[]): ConfigEntry[] => { + const entryLookup: { [entryId: string]: ConfigEntry } = {}; + for (const entry of entries) { + entryLookup[entry.entry_id] = entry; + } + return device.config_entries + .map((entry) => entryLookup[entry]) + .filter(Boolean); + } ); private _entities = memoizeOne( diff --git a/src/panels/config/integrations/ha-config-flow-card.ts b/src/panels/config/integrations/ha-config-flow-card.ts index 8509ab2e5f..77be858ca8 100644 --- a/src/panels/config/integrations/ha-config-flow-card.ts +++ b/src/panels/config/integrations/ha-config-flow-card.ts @@ -1,15 +1,10 @@ +import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; +import { mdiBookshelf, mdiCog, mdiDotsVertical, mdiOpenInNew } from "@mdi/js"; import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import { - mdiBookshelf, - mdiCog, - mdiDotsVertical, - mdiEyeOff, - mdiOpenInNew, -} from "@mdi/js"; -import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; import { fireEvent } from "../../../common/dom/fire_event"; +import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import { ATTENTION_SOURCES, DISCOVERY_SOURCES, @@ -17,13 +12,12 @@ import { localizeConfigFlowTitle, } from "../../../data/config_flow"; import type { IntegrationManifest } from "../../../data/integration"; -import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; import type { DataEntryFlowProgressExtended } from "./ha-config-integrations"; import "./ha-integration-action-card"; -import { documentationUrl } from "../../../util/documentation-url"; @customElement("ha-config-flow-card") export class HaConfigFlowCard extends LitElement { @@ -38,16 +32,10 @@ export class HaConfigFlowCard extends LitElement { return html` @@ -60,72 +48,75 @@ export class HaConfigFlowCard extends LitElement { }` )} > - - - ${this.flow.context.configuration_url - ? html` - - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.open_configuration_url" - )} - - - - ` - : ""} - ${this.manifest - ? html`` + : ""} + ${this.flow.context.configuration_url || this.manifest + ? html` + + ${this.flow.context.configuration_url + ? html` - - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.documentation" - )} - - - - ` - : ""} - ${DISCOVERY_SOURCES.includes(this.flow.context.source) && - this.flow.context.unique_id - ? html` - - ${this.hass.localize( - "ui.panel.config.integrations.ignore.ignore" - )} - - - ` - : ""} - + ? "_self" + : "_blank"} + > + + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.open_configuration_url" + )} + + + + ` + : ""} + ${this.manifest + ? html` + + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.documentation" + )} + + + + ` + : ""} + ` + : ""} `; } @@ -174,14 +165,6 @@ export class HaConfigFlowCard extends LitElement { } static styles = css` - .attention { - --state-color: var(--error-color); - --text-on-state-color: var(--text-primary-color); - } - .discovered { - --state-color: var(--primary-color); - --text-on-state-color: var(--text-primary-color); - } a { text-decoration: none; color: var(--primary-color); diff --git a/src/panels/config/integrations/ha-config-integration-page.ts b/src/panels/config/integrations/ha-config-integration-page.ts index cc4819c05f..45292826bc 100644 --- a/src/panels/config/integrations/ha-config-integration-page.ts +++ b/src/panels/config/integrations/ha-config-integration-page.ts @@ -543,8 +543,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { ]; if (item.reason) { this.hass.loadBackendTranslation("config", item.domain); - stateTextExtra = html`: - ${this.hass.localize( + stateTextExtra = html`${this.hass.localize( `component.${item.domain}.config.error.${item.reason}` ) || item.reason}`; } else { diff --git a/src/panels/config/integrations/ha-config-integrations-dashboard.ts b/src/panels/config/integrations/ha-config-integrations-dashboard.ts index 179167cd22..969c44bbbf 100644 --- a/src/panels/config/integrations/ha-config-integrations-dashboard.ts +++ b/src/panels/config/integrations/ha-config-integrations-dashboard.ts @@ -377,46 +377,57 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) { `} ${this._showIgnored - ? html`
- ${ignoredConfigEntries.map( - (entry: ConfigEntryExtended) => html` - - ` - )} -
` + ? html`

+ ${this.hass.localize( + "ui.panel.config.integrations.ignore.ignored" + )} +

+
+ ${ignoredConfigEntries.map( + (entry: ConfigEntryExtended) => html` + + ` + )} +
` : ""} ${configEntriesInProgress.length - ? html`
- ${configEntriesInProgress.map( - (flow: DataEntryFlowProgressExtended) => html` - - ` - )} -
` + ? html`

+ ${this.hass.localize("ui.panel.config.integrations.discovered")} +

+
+ ${configEntriesInProgress.map( + (flow: DataEntryFlowProgressExtended) => html` + + ` + )} +
` : ""} ${this._showDisabled - ? html`
- ${disabledConfigEntries.map( - (entry: ConfigEntryExtended) => html` - - ` - )} -
` + ? html`

+ ${this.hass.localize("ui.panel.config.integrations.disabled")} +

+
+ ${disabledConfigEntries.map( + (entry: ConfigEntryExtended) => html` + + ` + )} +
` : ""}
${integrations.length @@ -746,19 +757,18 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) { } .container { display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - grid-gap: 16px 16px; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-gap: 8px 8px; padding: 8px 16px 16px; } .container:last-of-type { margin-bottom: 64px; } - .container > * { - max-width: 500px; - } .empty-message { margin: auto; text-align: center; + grid-column-start: 1; + grid-column-end: -1; } .empty-message h1 { margin-bottom: 0; @@ -854,6 +864,9 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) { .menu-badge-container { position: relative; } + h1 { + margin: 8px 0 0 16px; + } ha-button-menu { color: var(--primary-text-color); } diff --git a/src/panels/config/integrations/ha-integration-action-card.ts b/src/panels/config/integrations/ha-integration-action-card.ts index bd81203095..0d708df2ef 100644 --- a/src/panels/config/integrations/ha-integration-action-card.ts +++ b/src/panels/config/integrations/ha-integration-action-card.ts @@ -1,9 +1,14 @@ import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; -import type { IntegrationManifest } from "../../../data/integration"; +import { + domainToName, + type IntegrationManifest, +} from "../../../data/integration"; import type { HomeAssistant } from "../../../types"; import "./ha-integration-header"; import "../../../components/ha-card"; +import { brandsUrl } from "../../../util/brands-url"; +import { haStyle } from "../../../resources/styles"; @customElement("ha-integration-action-card") export class HaIntegrationActionCard extends LitElement { @@ -22,49 +27,94 @@ export class HaIntegrationActionCard extends LitElement { protected render(): TemplateResult { return html` - - - +
+ +

${this.label}

+

+ ${this.localizedDomainName || + domainToName(this.hass.localize, this.domain, this.manifest)} +

+
-
+
+
`; } - static styles = css` - ha-card { - display: flex; - flex-direction: column; - height: 100%; - --ha-card-border-color: var(--state-color); - --mdc-theme-primary: var(--state-color); - } - .filler { - flex: 1; - } - .attention { - --state-color: var(--error-color); - --text-on-state-color: var(--text-primary-color); - } - .discovered { - --state-color: var(--primary-color); - --text-on-state-color: var(--text-primary-color); - } - .actions { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 6px 0; - height: 48px; - } - `; + private _onImageLoad(ev) { + ev.target.style.visibility = "initial"; + } + + private _onImageError(ev) { + ev.target.style.visibility = "hidden"; + } + + static styles = [ + haStyle, + css` + ha-card { + display: flex; + flex-direction: column; + height: 100%; + } + img { + width: 40px; + height: 40px; + } + h2 { + font-size: 16px; + font-weight: 400; + margin-top: 8px; + margin-bottom: 0; + } + h3 { + font-size: 14px; + margin: 0; + } + .header-button { + position: absolute; + top: 8px; + right: 8px; + } + .filler { + flex: 1; + } + .attention { + --state-color: var(--error-color); + --text-on-state-color: var(--text-primary-color); + } + .card-content { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + } + .card-actions { + border-top: none; + padding-top: 0; + padding-bottom: 16px; + justify-content: center; + display: flex; + } + :host ::slotted(*) { + margin-right: 8px; + } + :host ::slotted(:last-child) { + margin-right: 0; + } + `, + ]; } declare global { diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index 7866cd8343..acccb400a2 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -97,7 +97,13 @@ export class HaIntegrationCard extends LitElement { .hass=${this.hass} .domain=${this.domain} .localizedDomainName=${this.items[0].localized_domain_name} - .banner=${entryState !== "loaded" + .error=${ERROR_STATES.includes(entryState) + ? this.hass.localize( + `ui.panel.config.integrations.config_entry.state.${entryState}` + ) + : undefined} + .warning=${entryState !== "loaded" && + !ERROR_STATES.includes(entryState) ? this.hass.localize( `ui.panel.config.integrations.config_entry.state.${entryState}` ) @@ -317,9 +323,12 @@ export class HaIntegrationCard extends LitElement { --text-on-state-color: var(--primary-text-color); } .state-not-loaded { + opacity: 0.8; + --state-color: var(--warning-color); --state-message-color: var(--primary-text-color); } .state-setup { + opacity: 0.8; --state-message-color: var(--secondary-text-color); } :host(.highlight) ha-card { diff --git a/src/panels/config/integrations/ha-integration-header.ts b/src/panels/config/integrations/ha-integration-header.ts index 16085d5ead..44fc4497be 100644 --- a/src/panels/config/integrations/ha-integration-header.ts +++ b/src/panels/config/integrations/ha-integration-header.ts @@ -1,4 +1,5 @@ -import { LitElement, TemplateResult, css, html } from "lit"; +import { mdiAlertCircleOutline, mdiAlertOutline } from "@mdi/js"; +import { LitElement, TemplateResult, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import "../../../components/ha-svg-icon"; import { IntegrationManifest, domainToName } from "../../../data/integration"; @@ -9,9 +10,9 @@ import { brandsUrl } from "../../../util/brands-url"; export class HaIntegrationHeader extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public banner?: string; + @property() public error?: string; - @property() public label?: string; + @property() public warning?: string; @property() public localizedDomainName?: string; @@ -20,25 +21,11 @@ export class HaIntegrationHeader extends LitElement { @property({ attribute: false }) public manifest?: IntegrationManifest; protected render(): TemplateResult { - let primary: string; - let secondary: string | undefined; - const domainName = this.localizedDomainName || domainToName(this.hass.localize, this.domain, this.manifest); - if (this.label) { - primary = this.label; - secondary = - primary.toLowerCase() === domainName.toLowerCase() - ? undefined - : domainName; - } else { - primary = domainName; - } - return html` - ${!this.banner ? "" : html``}
-
${primary}
-
${secondary}
+
+ ${domainName} +
+ ${this.error + ? html`
+ ${this + .error} +
` + : this.warning + ? html`
+ ${this + .warning} +
` + : nothing}
@@ -71,16 +74,6 @@ export class HaIntegrationHeader extends LitElement { } static styles = css` - .banner { - background-color: var(--state-color); - color: var(--text-on-state-color); - text-align: center; - padding: 2px; - - /* Padding is subtracted for nested elements with border radiuses */ - border-top-left-radius: calc(var(--ha-card-border-radius, 12px) - 2px); - border-top-right-radius: calc(var(--ha-card-border-radius, 12px) - 2px); - } .header { display: flex; position: relative; @@ -101,29 +94,46 @@ export class HaIntegrationHeader extends LitElement { flex: 1; align-self: center; } - .header .info div { + .primary, + .warning, + .error { word-wrap: break-word; display: -webkit-box; -webkit-box-orient: vertical; - -webkit-line-clamp: 2; overflow: hidden; text-overflow: ellipsis; } - .header-button { - display: flex; - align-items: center; - justify-content: center; - width: 36px; - } .primary { font-size: 16px; font-weight: 400; word-break: break-word; color: var(--primary-text-color); + -webkit-line-clamp: 2; } - .secondary { + .hasError { + -webkit-line-clamp: 1; font-size: 14px; - color: var(--secondary-text-color); + } + .warning, + .error { + line-height: 20px; + --mdc-icon-size: 20px; + -webkit-line-clamp: 1; + font-size: 0.9em; + } + .error ha-svg-icon { + margin-right: 4px; + color: var(--error-color); + } + .warning ha-svg-icon { + margin-right: 4px; + color: var(--warning-color); + } + .header-button { + display: flex; + align-items: center; + justify-content: center; + width: 36px; } `; } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-groups-dashboard.ts b/src/panels/config/integrations/integration-panels/zha/zha-groups-dashboard.ts index 39f77d934b..0b756fe41d 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-groups-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-groups-dashboard.ts @@ -119,6 +119,7 @@ export class ZHAGroupsDashboard extends LitElement { .data=${this._formattedGroups(this._groups)} @row-click=${this._handleRowClicked} clickable + hasFab > { + if (contexts.length < 2) { + return []; + } + let total = 0; + for (const context of contexts) { + total += (context.dataset.data[context.dataIndex] as any).y; + } + if (total === 0) { + return []; + } + return [ + this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_gas_graph.total_consumed", + { num: formatNumber(total, locale), unit } + ), + ]; + }, }, }, filler: { diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts index 778ccb77d1..b1f8da63c0 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts @@ -236,6 +236,27 @@ export class HuiEnergySolarGraphCard context.parsed.y, locale )} kWh`, + footer: (contexts) => { + const production_contexts = contexts.filter( + (c) => c.dataset?.stack === "solar" + ); + if (production_contexts.length < 2) { + return []; + } + let total = 0; + for (const context of production_contexts) { + total += (context.dataset.data[context.dataIndex] as any).y; + } + if (total === 0) { + return []; + } + return [ + this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_solar_graph.total_produced", + { num: formatNumber(total, locale) } + ), + ]; + }, }, }, filler: { diff --git a/src/panels/lovelace/cards/energy/hui-energy-water-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-water-graph-card.ts index 41d4029c04..4fc102ebf2 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-water-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-water-graph-card.ts @@ -240,6 +240,24 @@ export class HuiEnergyWaterGraphCard context.parsed.y, locale )} ${unit}`, + footer: (contexts) => { + if (contexts.length < 2) { + return []; + } + let total = 0; + for (const context of contexts) { + total += (context.dataset.data[context.dataIndex] as any).y; + } + if (total === 0) { + return []; + } + return [ + this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_water_graph.total_consumed", + { num: formatNumber(total, locale), unit } + ), + ]; + }, }, }, filler: { diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index 37d5a4d27e..ec04f21acd 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -46,7 +46,7 @@ export class HuiCardOptions extends LitElement { @queryAssignedNodes() private _assignedNodes?: NodeListOf; - @property({ type: Boolean }) public showPosition = false; + @property({ type: Boolean }) public hidePosition = false; @storage({ key: "lovelaceClipboard", @@ -82,36 +82,38 @@ export class HuiCardOptions extends LitElement { >
- - ${this.showPosition - ? html` -
${this.path![1] + 1}
-
` + ${!this.hidePosition + ? html` + + +
${this.path![1] + 1}
+
+ + ` : nothing} - (msg: MessageBase): Promise; loadBackendTranslation( category: Parameters[2], - integration?: Parameters[3], + integrations?: Parameters[3], configFlow?: Parameters[4] ): Promise; loadFragmentTranslation(fragment: string): Promise;