Compare commits

..

16 Commits

Author SHA1 Message Date
Wendelin
ab0b1a4632 Add context groups 2026-04-08 16:05:03 +02:00
Aidan Timson
aab2304d86 Remove extra "Community:" prefix for add badge dialog (#51465)
Remove extra "Custom:" naming
2026-04-08 10:35:01 +00:00
Aidan Timson
c013f79826 Add links to logs in integration page (#51463)
* Add link to logs in integration page

* Join

* Add link to "Check the logs"

* Add to entry overflow when in error state
2026-04-08 13:34:59 +03:00
renovate[bot]
60236c2fee Update dependency @html-eslint/eslint-plugin to v0.59.0 (#51464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 11:26:42 +01:00
Aidan Timson
20d53a2659 Allow quick search for non-admins, while hiding inaccessible areas (#51456)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-04-08 09:50:36 +02:00
Mihail Sîrbu
6dbc38386c Add apps info page for non-HAOS installations (#30364)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-04-08 07:02:47 +00:00
renovate[bot]
ce5a19caa8 Update dependency marked to v17.0.6 (#51460)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 08:59:33 +02:00
Wendelin
2cda06e7a6 Introduce ha-progress-bar (#51453)
* Replace mwc-linear-progress with ha-progress-bar

* Update src/panels/lovelace/cards/hui-media-control-card.ts

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Remove duplicate import of ha-slider in hui-media-control-card.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-04-08 09:40:42 +03:00
renovate[bot]
65485ce8c9 Update dependency fuse.js to v7.3.0 (#51457)
* Update dependency fuse.js to v7.3.0

* type fix

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-04-08 05:28:37 +00:00
GeorgeZ83
b73ae60cea Fix media browser dialog window (#51423)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-04-07 14:52:16 +00:00
Petar Petrov
cef35c6c23 Center energy dashboard bar charts on period midpoint (#30325) 2026-04-07 16:51:35 +02:00
Petar Petrov
6b9685ec9f Fix time condition summary using "and" instead of "or" for midnight-crossing ranges (#51452) 2026-04-07 16:47:17 +02:00
Petar Petrov
fc9289dc05 Fix incorrect timezone in automation time trigger/condition descriptions (#51454) 2026-04-07 14:09:28 +00:00
Aidan Timson
2a2bca2a61 Handle lazy loaded entity registry when editing scripts from more info (#51438)
* Handle lazy loaded entity registry when editing scripts from more info

* Remove extra check

* Fix type of mixin
2026-04-07 17:07:21 +03:00
Petar Petrov
1eda51ddbc Allow customizing initial map view with latitude, longitude, and zoom (#51444)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-07 15:07:13 +01:00
Wendelin
22738f6d77 Remove fab (#51448)
* Remove ha-fab replace with ha-button

* Remove import of ha-fab component

* Remove mwc-fab
2026-04-07 15:46:36 +03:00
163 changed files with 2136 additions and 1775 deletions

View File

@@ -57,7 +57,7 @@ Check the [webawesome documentation](https://webawesome.com/docs/components/butt
| ---------- | ---------------------------------------------- | -------- | --------------------------------------------------------------------------------- |
| appearance | "accent"/"filled"/"plain" | "accent" | Sets the button appearance. |
| variants | "brand"/"danger"/"neutral"/"warning"/"success" | "brand" | Sets the button color variant. "brand" is default. |
| size | "small"/"medium" | "medium" | Sets the button size. |
| size | "small"/"medium"/"large" | "medium" | Sets the button size. |
| loading | Boolean | false | Shows a loading indicator instead of the buttons label and disable buttons click. |
| disabled | Boolean | false | Disables the button and prevents user interaction. |

View File

@@ -488,79 +488,6 @@ const SCHEMAS: {
},
],
},
{
title: "Tabs",
translations: {
settings: "Settings",
tab_general: "General",
tab_appearance: "Appearance",
name: "Name",
entity: "Entity",
theme: "Theme",
state_color: "Color on state",
},
schema: [
{
type: "tabs",
name: "settings",
tabs: [
{
name: "general",
icon: "mdi:cog",
schema: [
{ name: "name", selector: { text: {} } },
{ name: "entity", required: true, selector: { entity: {} } },
],
},
{
name: "appearance",
icon: "mdi:palette",
schema: [
{ name: "theme", selector: { theme: {} } },
{ name: "state_color", selector: { boolean: {} } },
],
},
],
},
],
},
{
title: "Tabs (compact)",
translations: {
settings: "Settings",
tab_general: "General",
tab_appearance: "Appearance",
name: "Name",
entity: "Entity",
theme: "Theme",
state_color: "Color on state",
},
schema: [
{
type: "tabs",
name: "settings",
fill_tabs: false,
tabs: [
{
name: "general",
icon: "mdi:cog",
schema: [
{ name: "name", selector: { text: {} } },
{ name: "entity", required: true, selector: { entity: {} } },
],
},
{
name: "appearance",
icon: "mdi:palette",
schema: [
{ name: "theme", selector: { theme: {} } },
{ name: "state_color", selector: { boolean: {} } },
],
},
],
},
],
},
];
@customElement("demo-components-ha-form")
@@ -608,12 +535,8 @@ class DemoHaForm extends LitElement {
.error=${info.error}
.disabled=${this.disabled[idx]}
.computeError=${(error) => translations[error] || error}
.computeLabel=${(schema, _data, options) => {
if (options?.tab) {
return translations[`tab_${options.tab}`] || options.tab;
}
return translations[schema.name] || schema.name;
}}
.computeLabel=${(schema) =>
translations[schema.name] || schema.name}
.computeHelper=${() => "Helper text"}
@value-changed=${this._handleValueChanged}
.sampleIdx=${idx}

View File

@@ -10,7 +10,7 @@ import "../../../../src/components/input/ha-input";
import "../../../../src/components/input/ha-input-copy";
import "../../../../src/components/input/ha-input-multi";
import "../../../../src/components/input/ha-input-search";
import { localizeContext } from "../../../../src/data/context";
import { internationalizationContext } from "../../../../src/data/context/context";
const LOCALIZE_KEYS: Record<string, string> = {
"ui.common.copy": "Copy",
@@ -26,11 +26,19 @@ const LOCALIZE_KEYS: Record<string, string> = {
export class DemoHaInput extends LitElement {
constructor() {
super();
// Provides localizeContext for ha-input-copy, ha-input-multi and ha-input-search
// Provides internationalizationContext for ha-input-copy, ha-input-multi and ha-input-search
// eslint-disable-next-line no-new
new ContextProvider(this, {
context: localizeContext,
initialValue: ((key: string) => LOCALIZE_KEYS[key] ?? key) as any,
context: internationalizationContext,
initialValue: {
localize: ((key: string) => LOCALIZE_KEYS[key] ?? key) as any,
language: "en",
selectedLanguage: null,
locale: {} as any,
translationMetadata: {} as any,
loadBackendTranslation: (async () => (key: string) => key) as any,
loadFragmentTranslation: (async () => (key: string) => key) as any,
},
});
}

View File

@@ -1,6 +1,5 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { mdiArrowCollapseDown, mdiDownload } from "@mdi/js";
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
import { mdiArrowCollapseDown, mdiDownload } from "@mdi/js";
import { LitElement, type PropertyValues, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";

View File

@@ -1,4 +1,3 @@
import "@material/mwc-linear-progress";
import { mdiOpenInNew } from "@mdi/js";
import { css, html, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -8,6 +7,7 @@ import "../../src/components/ha-button";
import "../../src/components/ha-fade-in";
import "../../src/components/ha-spinner";
import "../../src/components/ha-svg-icon";
import "../../src/components/progress/ha-progress-bar";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import "../../src/onboarding/onboarding-welcome-links";
import { onBoardingStyles } from "../../src/onboarding/styles";
@@ -60,7 +60,7 @@ class HaLandingPage extends LandingPageBaseElement {
${!networkIssue && !this._supervisorError
? html`
<p>${this.localize("subheader")}</p>
<mwc-linear-progress indeterminate></mwc-linear-progress>
<ha-progress-bar indeterminate></ha-progress-bar>
`
: nothing}
${networkIssue || this._networkInfoError

View File

@@ -65,10 +65,8 @@
"@material/mwc-checkbox": "0.27.0",
"@material/mwc-dialog": "0.27.0",
"@material/mwc-drawer": "0.27.0",
"@material/mwc-fab": "0.27.0",
"@material/mwc-floating-label": "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-linear-progress": "0.27.0",
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"@material/mwc-radio": "0.27.0",
"@material/mwc-select": "0.27.0",
@@ -100,7 +98,7 @@
"dialog-polyfill": "0.5.6",
"echarts": "6.0.0",
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.2.0",
"fuse.js": "7.3.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "7.0.0",
"hls.js": "1.6.15",
@@ -114,7 +112,7 @@
"lit": "3.3.2",
"lit-html": "3.3.2",
"luxon": "3.7.2",
"marked": "17.0.5",
"marked": "17.0.6",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.4",
"object-hash": "3.0.0",
@@ -144,7 +142,7 @@
"@bundle-stats/plugin-webpack-filter": "4.22.0",
"@eslint/eslintrc": "3.3.5",
"@eslint/js": "10.0.1",
"@html-eslint/eslint-plugin": "0.58.1",
"@html-eslint/eslint-plugin": "0.59.0",
"@lokalise/node-api": "15.6.1",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.1.0",

View File

@@ -21,6 +21,9 @@ export const filterNavigationPages = (
if (page.path === "#external-app-configuration") {
return hass.auth.external?.config.hasSettingsScreen;
}
if (page.adminOnly && !hass.user?.is_admin) {
return false;
}
// Only show Bluetooth page if there are Bluetooth config entries
if (page.component === "bluetooth") {
return options.hasBluetoothConfigEntries ?? false;

View File

@@ -7,7 +7,8 @@ export type LeafletModuleType = typeof import("leaflet");
export type LeafletDrawModuleType = typeof import("leaflet-draw");
export const setupLeafletMap = async (
mapElement: HTMLElement
mapElement: HTMLElement,
initialView?: { latitude: number; longitude: number; zoom?: number }
): Promise<[Map, LeafletModuleType, TileLayer]> => {
if (!mapElement.parentNode) {
throw new Error("Cannot setup Leaflet map on disconnected element");
@@ -32,7 +33,12 @@ export const setupLeafletMap = async (
markerClusterStyle.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(markerClusterStyle);
map.setView([52.3731339, 4.8903147], 13);
if (initialView) {
map.setView(
[initialView.latitude, initialView.longitude],
initialView.zoom ?? 13
);
}
const tileLayer = createTileLayer(Leaflet).addTo(map);

View File

@@ -18,15 +18,16 @@ import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { ensureArray } from "../../common/array/ensure-array";
import { getAllGraphColors } from "../../common/color/colors";
import { transform } from "../../common/decorators/transform";
import type { HASSDomEvent } from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import { listenMediaQuery } from "../../common/dom/media_query";
import { afterNextRender } from "../../common/util/render-status";
import { filterXSS } from "../../common/util/xss";
import { themesContext } from "../../data/context";
import { uiContext } from "../../data/context/context";
import type { Themes } from "../../data/ws-themes";
import type { ECOption } from "../../resources/echarts/echarts";
import type { HomeAssistant } from "../../types";
import type { HomeAssistant, HomeAssistantUI } from "../../types";
import { isMac } from "../../util/is_mac";
import "../chips/ha-assist-chip";
import "../ha-icon-button";
@@ -74,8 +75,11 @@ export class HaChartBase extends LitElement {
public extraComponents?: any[];
@state()
@consume({ context: themesContext, subscribe: true })
_themes!: Themes;
@consume({ context: uiContext, subscribe: true })
@transform<HomeAssistantUI, Themes>({
transformer: ({ themes }) => themes,
})
private _themes!: Themes;
@state() private _isZoomed = false;

View File

@@ -1,8 +1,8 @@
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, state } from "lit/decorators";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiRestart } from "@mdi/js";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import type {
@@ -12,12 +12,12 @@ import type {
} from "../../data/history";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import type { StateHistoryChartLine } from "./state-history-chart-line";
import type { StateHistoryChartTimeline } from "./state-history-chart-timeline";
import "../ha-fab";
import "../ha-button";
import "../ha-svg-icon";
import "./state-history-chart-line";
import type { StateHistoryChartLine } from "./state-history-chart-line";
import "./state-history-chart-timeline";
import type { StateHistoryChartTimeline } from "./state-history-chart-timeline";
const CANVAS_TIMELINE_ROWS_CHUNK = 10; // Split up the canvases to avoid hitting the render limit
@@ -150,16 +150,14 @@ export class StateHistoryCharts extends LitElement {
this._renderHistoryItem(item, index)
)}`}
${this.syncCharts && this._hasZoomedCharts
? html`<ha-fab
slot="fab"
? html`<ha-button
size="large"
class="reset-button"
.label=${this.hass.localize(
"ui.components.history_charts.zoom_reset"
)}
@click=${this._handleGlobalZoomReset}
>
<ha-svg-icon slot="icon" .path=${mdiRestart}></ha-svg-icon>
</ha-fab>`
<ha-svg-icon slot="start" .path=${mdiRestart}></ha-svg-icon>
${this.hass.localize("ui.components.history_charts.zoom_reset")}
</ha-button>`
: nothing}
`;
}
@@ -448,6 +446,7 @@ export class StateHistoryCharts extends LitElement {
bottom: calc(24px + var(--safe-area-inset-bottom));
right: calc(24px + var(--safe-area-inset-bottom));
z-index: 1;
--ha-button-box-shadow: var(--ha-box-shadow-l);
}
`;
}

View File

@@ -16,17 +16,17 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { STRINGS_SEPARATOR_DOT } from "../../common/const";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event";
import type {
HASSDomCurrentTargetEvent,
HASSDomTargetEvent,
} from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare";
import type { LocalizeFunc } from "../../common/translations/localize";
import { debounce } from "../../common/util/debounce";
import { groupBy } from "../../common/util/group-by";
import { nextRender } from "../../common/util/render-status";
import { localeContext, localizeContext } from "../../data/context";
import { internationalizationContext } from "../../data/context/context";
import type { FrontendLocaleData } from "../../data/translation";
import { haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
@@ -112,12 +112,8 @@ const AUTO_FOCUS_ALLOWED_ACTIVE_TAGS = ["BODY", "HTML", "HOME-ASSISTANT"];
@customElement("ha-data-table")
export class HaDataTable extends LitElement {
@state()
@consume({ context: localizeContext, subscribe: true })
private _localize?: ContextType<typeof localizeContext>;
@state()
@consume({ context: localeContext, subscribe: true })
private _locale?: ContextType<typeof localeContext>;
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
@property({ type: Boolean }) public narrow = false;
@@ -528,7 +524,9 @@ export class HaDataTable extends LitElement {
<div class="mdc-data-table__row" role="row">
<div class="mdc-data-table__cell grows center" role="cell">
${this.noDataText ||
this._localize?.("ui.components.data-table.no-data") ||
this._i18n?.localize?.(
"ui.components.data-table.no-data"
) ||
"No data"}
</div>
</div>
@@ -542,8 +540,8 @@ export class HaDataTable extends LitElement {
@scroll=${this._saveScrollPos}
.items=${this._groupData(
this._filteredData,
this._localize,
this._locale,
this._i18n?.localize,
this._i18n?.locale,
this.appendRow,
this.groupColumn,
this.groupOrder,
@@ -713,7 +711,7 @@ export class HaDataTable extends LitElement {
this._sortColumns[this.sortColumn],
this.sortDirection,
this.sortColumn,
this._locale?.language
this._i18n?.locale?.language
)
: filteredData;
@@ -893,8 +891,8 @@ export class HaDataTable extends LitElement {
const groupedData = this._groupData(
this._filteredData,
this._localize,
this._locale,
this._i18n?.localize,
this._i18n?.locale,
this.appendRow,
this.groupColumn,
this.groupOrder,

View File

@@ -3,6 +3,7 @@ import { consume, type ContextType } from "@lit/context";
import type { ActionDetail } from "@material/mwc-list";
import { mdiCalendarToday } from "@mdi/js";
import "cally";
import type { HassConfig } from "home-assistant-js-websocket/dist/types";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
@@ -12,16 +13,16 @@ import {
formatDateYear,
formatISODateOnly,
} from "../../common/datetime/format_date";
import { transform } from "../../common/decorators/transform";
import { fireEvent } from "../../common/dom/fire_event";
import {
configContext,
localeContext,
localizeContext,
} from "../../data/context";
internationalizationContext,
} from "../../data/context/context";
import { TimeZone } from "../../data/translation";
import { MobileAwareMixin } from "../../mixins/mobile-aware-mixin";
import { haStyleScrollbar } from "../../resources/styles";
import type { ValueChangedEvent } from "../../types";
import type { HomeAssistantConfig, ValueChangedEvent } from "../../types";
import "../chips/ha-chip-set";
import "../chips/ha-filter-chip";
import type { HaFilterChip } from "../chips/ha-filter-chip";
@@ -48,16 +49,15 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
public timePicker = false;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@state()
@consume({ context: localeContext, subscribe: true })
private locale!: ContextType<typeof localeContext>;
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: configContext, subscribe: true })
private hassConfig!: ContextType<typeof configContext>;
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _hassConfig!: HassConfig;
/** used to show month in calendar-range header */
@state() private _pickerMonth?: string;
@@ -87,12 +87,20 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
? formatCallyDateRange(
this.startDate,
this.endDate,
this.locale,
this.hassConfig
this._i18n?.locale,
this._hassConfig
)
: undefined;
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(
date,
this._i18n.locale,
this._hassConfig
);
if (this.timePicker && this.startDate && this.endDate) {
this._timeValue = {
@@ -144,12 +152,12 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
<div class="range">
<calendar-range
.value=${this._dateValue}
.locale=${this.locale.language}
.locale=${this._i18n.locale.language}
.focusedDate=${this._focusDate}
@focusday=${this._focusChanged}
@change=${this._handleChange}
show-outside-days
.firstDayOfWeek=${firstWeekdayIndex(this.locale)}
.firstDayOfWeek=${firstWeekdayIndex(this._i18n.locale)}
>
<ha-icon-button-prev
tabindex="-1"
@@ -162,7 +170,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
<ha-icon-button
@click=${this._focusToday}
.path=${mdiCalendarToday}
.label=${this.localize("ui.dialogs.date-picker.today")}
.label=${this._i18n.localize("ui.dialogs.date-picker.today")}
></ha-icon-button>
</div>
<ha-icon-button-next
@@ -176,9 +184,9 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
<div class="times">
<ha-time-input
.value=${`${this._timeValue.from.hours}:${this._timeValue.from.minutes}`}
.locale=${this.locale}
.locale=${this._i18n.locale}
@value-changed=${this._handleChangeTime}
.label=${this.localize(
.label=${this._i18n.localize(
"ui.components.date-range-picker.time_from"
)}
id="from"
@@ -187,9 +195,9 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
></ha-time-input>
<ha-time-input
.value=${`${this._timeValue.to.hours}:${this._timeValue.to.minutes}`}
.locale=${this.locale}
.locale=${this._i18n.locale}
@value-changed=${this._handleChangeTime}
.label=${this.localize(
.label=${this._i18n.localize(
"ui.components.date-range-picker.time_to"
)}
id="to"
@@ -203,19 +211,33 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
</div>
<div class="footer">
<ha-button appearance="plain" @click=${this._cancel}
>${this.localize("ui.common.cancel")}</ha-button
>${this._i18n.localize("ui.common.cancel")}</ha-button
>
<ha-button .disabled=${!this._dateValue} @click=${this._save}
>${this.localize("ui.components.date-range-picker.select")}</ha-button
>${this._i18n.localize(
"ui.components.date-range-picker.select"
)}</ha-button
>
</div>`;
}
private _focusToday() {
const date = new Date();
this._focusDate = formatISODateOnly(date, this.locale, this.hassConfig);
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
this._focusDate = formatISODateOnly(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(
date,
this._i18n.locale,
this._hassConfig
);
}
private _cancel() {
@@ -255,12 +277,12 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
}
}
if (this.locale.time_zone === TimeZone.server) {
if (this._i18n.locale.time_zone === TimeZone.server) {
startDate = new Date(
new TZDate(startDate, this.hassConfig.time_zone).getTime()
new TZDate(startDate, this._hassConfig.time_zone).getTime()
);
endDate = new Date(
new TZDate(endDate, this.hassConfig.time_zone).getTime()
new TZDate(endDate, this._hassConfig.time_zone).getTime()
);
}
@@ -286,8 +308,16 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
private _focusChanged(ev: CustomEvent<Date>) {
const date = ev.detail;
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(
date,
this._i18n.locale,
this._hassConfig
);
this._focusDate = undefined;
}

View File

@@ -3,6 +3,7 @@ import { consume, type ContextType } from "@lit/context";
import { mdiCalendar } from "@mdi/js";
import "cally";
import { isThisYear } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket/dist/types";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@@ -14,12 +15,13 @@ import {
formatShortDateTime,
formatShortDateTimeWithYear,
} from "../../common/datetime/format_date_time";
import { transform } from "../../common/decorators/transform";
import { fireEvent } from "../../common/dom/fire_event";
import {
configContext,
localeContext,
localizeContext,
} from "../../data/context";
internationalizationContext,
} from "../../data/context/context";
import type { HomeAssistantConfig } from "../../types";
import "../ha-bottom-sheet";
import "../ha-icon-button";
import "../ha-icon-button-next";
@@ -43,16 +45,15 @@ const EXTENDED_RANGE_KEYS: DateRange[] = [
@customElement("ha-date-range-picker")
export class HaDateRangePicker extends LitElement {
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@state()
@consume({ context: localeContext, subscribe: true })
private locale!: ContextType<typeof localeContext>;
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: configContext, subscribe: true })
private hassConfig!: ContextType<typeof configContext>;
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _hassConfig!: HassConfig;
@property({ attribute: false }) public startDate!: Date;
@@ -117,8 +118,8 @@ export class HaDateRangePicker extends LitElement {
this._ranges = {};
rangeKeys.forEach((key) => {
this._ranges![
this.localize(`ui.components.date-range-picker.ranges.${key}`)
] = calcDateRange(this.locale, this.hassConfig, key);
this._i18n.localize(`ui.components.date-range-picker.ranges.${key}`)
] = calcDateRange(this._i18n.locale, this._hassConfig, key);
});
}
@@ -140,41 +141,43 @@ export class HaDateRangePicker extends LitElement {
.value=${(isThisYear(this.startDate)
? formatShortDateTime(
this.startDate,
this.locale,
this.hassConfig
this._i18n.locale,
this._hassConfig
)
: formatShortDateTimeWithYear(
this.startDate,
this.locale,
this.hassConfig
this._i18n.locale,
this._hassConfig
)) +
(window.innerWidth >= 459 ? " - " : " - \n") +
(isThisYear(this.endDate)
? formatShortDateTime(
this.endDate,
this.locale,
this.hassConfig
this._i18n.locale,
this._hassConfig
)
: formatShortDateTimeWithYear(
this.endDate,
this.locale,
this.hassConfig
this._i18n.locale,
this._hassConfig
))}
.label=${this.localize(
.label=${this._i18n.localize(
"ui.components.date-range-picker.start_date"
) +
" - " +
this.localize("ui.components.date-range-picker.end_date")}
this._i18n.localize(
"ui.components.date-range-picker.end_date"
)}
.disabled=${this.disabled}
readonly
></ha-textarea>
<ha-icon-button-prev
.label=${this.localize("ui.common.previous")}
.label=${this._i18n.localize("ui.common.previous")}
@click=${this._handlePrev}
>
</ha-icon-button-prev>
<ha-icon-button-next
.label=${this.localize("ui.common.next")}
.label=${this._i18n.localize("ui.common.next")}
@click=${this._handleNext}
>
</ha-icon-button-next>`
@@ -182,7 +185,7 @@ export class HaDateRangePicker extends LitElement {
@click=${this._openPicker}
.disabled=${this.disabled}
id="field"
.label=${this.localize(
.label=${this._i18n.localize(
"ui.components.date-range-picker.select_date_range"
)}
.path=${mdiCalendar}
@@ -290,8 +293,8 @@ export class HaDateRangePicker extends LitElement {
this.startDate,
this.endDate,
forward,
this.locale,
this.hassConfig
this._i18n.locale,
this._hassConfig
);
this.startDate = start;
this.endDate = end;

View File

@@ -2,6 +2,7 @@ import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume, type ContextType } from "@lit/context";
import { mdiBackspace, mdiCalendarToday } from "@mdi/js";
import "cally";
import type { HassConfig } from "home-assistant-js-websocket/dist/types";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import {
@@ -10,12 +11,13 @@ import {
formatDateYear,
formatISODateOnly,
} from "../../common/datetime/format_date";
import { transform } from "../../common/decorators/transform";
import {
configContext,
localeContext,
localizeContext,
} from "../../data/context";
internationalizationContext,
} from "../../data/context/context";
import { DialogMixin } from "../../dialogs/dialog-mixin";
import type { HomeAssistantConfig } from "../../types";
import "../ha-button";
import type { DatePickerDialogParams } from "../ha-date-input";
import "../ha-dialog";
@@ -40,16 +42,15 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
LitElement
) {
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@state()
@consume({ context: localeContext, subscribe: true })
private locale!: ContextType<typeof localeContext>;
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: configContext, subscribe: true })
private hassConfig!: ContextType<typeof configContext>;
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _hassConfig!: HassConfig;
@state() private _value?: {
year: string;
@@ -74,14 +75,26 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
? new Date(`${this.params.value.split("T")[0]}T00:00:00`)
: new Date();
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
this._pickerYear = formatDateYear(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._value = this.params.value
? {
year: this._pickerYear,
title: formatDateShort(date, this.locale, this.hassConfig),
dateString: formatISODateOnly(date, this.locale, this.hassConfig),
title: formatDateShort(date, this._i18n.locale, this._hassConfig),
dateString: formatISODateOnly(
date,
this._i18n.locale,
this._hassConfig
),
}
: undefined;
}
@@ -95,7 +108,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
open
width="small"
.headerTitle=${this._value?.title ||
this.localize("ui.dialogs.date-picker.title")}
this._i18n.localize("ui.dialogs.date-picker.title")}
.headerSubtitle=${this._value?.year}
header-subtitle-position="above"
>
@@ -103,7 +116,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
? html`
<ha-icon-button
.path=${mdiBackspace}
.label=${this.localize("ui.dialogs.date-picker.clear")}
.label=${this._i18n.localize("ui.dialogs.date-picker.clear")}
slot="headerActionItems"
@click=${this._clear}
></ha-icon-button>
@@ -131,7 +144,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
<ha-icon-button
@click=${this._setToday}
.path=${mdiCalendarToday}
.label=${this.localize("ui.dialogs.date-picker.today")}
.label=${this._i18n.localize("ui.dialogs.date-picker.today")}
></ha-icon-button>
</div>
<ha-icon-button-next tabindex="-1" slot="next"></ha-icon-button-next>
@@ -143,10 +156,10 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
slot="secondaryAction"
@click=${this.closeDialog}
>
${this.localize("ui.common.cancel")}
${this._i18n.localize("ui.common.cancel")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._setValue}>
${this.localize("ui.common.ok")}
${this._i18n.localize("ui.common.ok")}
</ha-button>
</ha-dialog-footer>
</ha-dialog>`;
@@ -164,23 +177,39 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
? new Date(`${value.split("T")[0]}T00:00:00`)
: new Date();
this._value = {
year: formatDateYear(date, this.locale, this.hassConfig),
title: formatDateShort(date, this.locale, this.hassConfig),
year: formatDateYear(date, this._i18n.locale, this._hassConfig),
title: formatDateShort(date, this._i18n.locale, this._hassConfig),
dateString:
value || formatISODateOnly(date, this.locale, this.hassConfig),
value || formatISODateOnly(date, this._i18n.locale, this._hassConfig),
};
if (setFocusDay) {
this._focusDate = this._value.dateString;
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(
date,
this._i18n.locale,
this._hassConfig
);
}
}
private _focusChanged(ev: CustomEvent<Date>) {
const date = ev.detail;
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(
date,
this._i18n.locale,
this._hassConfig
);
this._focusDate = undefined;
}

View File

@@ -4,7 +4,7 @@ import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { fullEntitiesContext } from "../../data/context";
import { fullEntitiesContext } from "../../data/context/context";
import type { DeviceAutomation } from "../../data/device/device_automation";
import {
deviceAutomationsEqual,

View File

@@ -27,7 +27,7 @@ export type Appearance = "accent" | "filled" | "outlined" | "plain";
* @cssprop --ha-button-height - The height of the button.
* @cssprop --ha-button-border-radius - The border radius of the button. defaults to `var(--ha-border-radius-pill)`.
*
* @attr {("small"|"medium")} size - Sets the button size.
* @attr {("small"|"medium"|"large")} size - Sets the button size.
* @attr {("brand"|"neutral"|"danger"|"warning"|"success")} variant - Sets the button color variant. "primary" is default.
* @attr {("accent"|"filled"|"plain")} appearance - Sets the button appearance.
* @attr {boolean} loading - shows a loading indicator instead of the buttons label and disable buttons click.
@@ -62,6 +62,7 @@ export class HaButton extends Button {
transition: background-color var(--ha-animation-duration-fast)
ease-out;
text-wrap: wrap;
box-shadow: var(--ha-button-box-shadow);
}
:host([size="small"]) .button {
@@ -73,6 +74,14 @@ export class HaButton extends Button {
--wa-form-control-padding-inline: var(--ha-space-3);
}
:host([size="large"]) .button {
--wa-form-control-height: var(
--ha-button-height,
var(--button-height, 48px)
);
font-size: var(--ha-font-size-l);
}
:host([variant="brand"]) {
--button-color-fill-normal-active: var(
--ha-color-fill-primary-normal-active

View File

@@ -7,7 +7,7 @@ import memoizeOne from "memoize-one";
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeKeys } from "../common/translations/localize";
import { localizeContext } from "../data/context";
import { internationalizationContext } from "../data/context/context";
import type { UiColorExtraOption } from "../data/selector";
import type { ValueChangedEvent } from "../types";
import "./ha-combo-box-item";
@@ -55,8 +55,8 @@ export class HaColorPicker extends LitElement {
@property({ type: Boolean }) public required = false;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
render() {
const effectiveValue = this.value ?? this.defaultColor ?? "";
@@ -73,7 +73,7 @@ export class HaColorPicker extends LitElement {
.rowRenderer=${this._rowRenderer}
.valueRenderer=${this._valueRenderer}
@value-changed=${this._valueChanged}
.notFoundLabel=${this.localize?.(
.notFoundLabel=${this._i18n?.localize?.(
"ui.components.color-picker.no_colors_found"
)}
.getAdditionalItems=${this._getAdditionalItems}
@@ -103,7 +103,7 @@ export class HaColorPicker extends LitElement {
{
id: searchString,
primary:
this.localize?.("ui.components.color-picker.custom_color") ||
this._i18n?.localize?.("ui.components.color-picker.custom_color") ||
"Custom color",
secondary: searchString,
},
@@ -130,14 +130,15 @@ export class HaColorPicker extends LitElement {
const items: PickerComboBoxItem[] = [];
const defaultSuffix =
this.localize?.("ui.components.color-picker.default") || "Default";
this._i18n?.localize?.("ui.components.color-picker.default") ||
"Default";
const addDefaultSuffix = (label: string, isDefault: boolean) =>
isDefault && defaultSuffix ? `${label} (${defaultSuffix})` : label;
if (includeNone) {
const noneLabel =
this.localize?.("ui.components.color-picker.none") || "None";
this._i18n?.localize?.("ui.components.color-picker.none") || "None";
items.push({
id: "none",
primary: addDefaultSuffix(noneLabel, defaultColor === "none"),
@@ -147,7 +148,7 @@ export class HaColorPicker extends LitElement {
if (includeState) {
const stateLabel =
this.localize?.("ui.components.color-picker.state") || "State";
this._i18n?.localize?.("ui.components.color-picker.state") || "State";
items.push({
id: "state",
primary: addDefaultSuffix(stateLabel, defaultColor === "state"),
@@ -170,7 +171,7 @@ export class HaColorPicker extends LitElement {
Array.from(THEME_COLORS).forEach((color) => {
const themeLabel =
this.localize?.(
this._i18n?.localize?.(
`ui.components.color-picker.colors.${color}` as LocalizeKeys
) || color;
items.push({
@@ -227,7 +228,7 @@ export class HaColorPicker extends LitElement {
return html`
<ha-svg-icon slot="start" .path=${mdiInvertColorsOff}></ha-svg-icon>
<span slot="headline">
${this.localize?.("ui.components.color-picker.none") || "None"}
${this._i18n?.localize?.("ui.components.color-picker.none") || "None"}
</span>
`;
}
@@ -235,7 +236,8 @@ export class HaColorPicker extends LitElement {
return html`
<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>
<span slot="headline">
${this.localize?.("ui.components.color-picker.state") || "State"}
${this._i18n?.localize?.("ui.components.color-picker.state") ||
"State"}
</span>
`;
}
@@ -243,7 +245,7 @@ export class HaColorPicker extends LitElement {
const extraOption = this.extraOptions?.find((o) => o.value === value);
const label =
extraOption?.label ||
this.localize?.(
this._i18n?.localize?.(
`ui.components.color-picker.colors.${value}` as LocalizeKeys
) ||
value;

View File

@@ -14,7 +14,7 @@ import { ifDefined } from "lit/directives/if-defined";
import type { HASSDomEvent } from "../common/dom/fire_event";
import { fireEvent } from "../common/dom/fire_event";
import { withViewTransition } from "../common/util/view-transition";
import { localizeContext } from "../data/context";
import { internationalizationContext } from "../data/context/context";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { haStyleScrollbar } from "../resources/styles";
import "./ha-dialog-header";
@@ -123,13 +123,13 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
@query(".body") public bodyContainer!: HTMLDivElement;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
// disabled till iOS app fix the "focus_element" implementation
// @state()
// @consume({ context: authContext, subscribe: true })
// private auth?: ContextType<typeof authContext>;
// @consume({ context: configContext, subscribe: true })
// private _hassConfig?: ContextType<typeof configContext>;
@state()
private _bodyScrolled = false;
@@ -184,7 +184,7 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-dialog="close"
.label=${this.localize?.("ui.common.close") ?? "Close"}
.label=${this._i18n?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button>
</slot>
@@ -222,13 +222,13 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
requestAnimationFrame(() => {
// disabled till iOS app fix the "focus_element" implementation
// if (this.auth?.external && isIosApp(this.auth.external)) {
// if (this._hassConfig?.auth.external && isIosApp(this._hassConfig.auth.external)) {
// const element = this.querySelector("[autofocus]");
// if (element !== null) {
// if (!element.id) {
// element.id = "ha-dialog-autofocus";
// }
// this.auth.external.fireMessage({
// this._hassConfig.auth.external.fireMessage({
// type: "focus_element",
// payload: {
// element_id: element.id,

View File

@@ -3,11 +3,10 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import {
authContext,
configContext,
connectionContext,
themesContext,
} from "../data/context";
uiContext,
} from "../data/context/context";
import {
DEFAULT_DOMAIN_ICON,
domainIcon,
@@ -38,12 +37,8 @@ export class HaDomainIcon extends LitElement {
private _connection?: ContextType<typeof connectionContext>;
@state()
@consume({ context: themesContext, subscribe: true })
private _themes?: ContextType<typeof themesContext>;
@state()
@consume({ context: authContext, subscribe: true })
private _auth?: ContextType<typeof authContext>;
@consume({ context: uiContext, subscribe: true })
private _hassUi?: ContextType<typeof uiContext>;
protected render() {
if (this.icon) {
@@ -59,8 +54,8 @@ export class HaDomainIcon extends LitElement {
}
const icon = domainIcon(
this._connection,
this._hassConfig,
this._connection.connection,
this._hassConfig.config,
this.domain,
this.deviceClass,
this.state
@@ -86,9 +81,9 @@ export class HaDomainIcon extends LitElement {
{
domain: this.domain!,
type: "icon",
darkOptimized: this._themes?.darkMode,
darkOptimized: this._hassUi?.themes.darkMode,
},
this._auth?.data.hassUrl
this._hassConfig?.auth.data.hassUrl
);
return html`
<img

View File

@@ -1,60 +0,0 @@
import { FabBase } from "@material/mwc-fab/mwc-fab-base";
import { styles } from "@material/mwc-fab/mwc-fab.css";
import { customElement } from "lit/decorators";
import { css } from "lit";
import { mainWindow } from "../common/dom/get_main_window";
@customElement("ha-fab")
export class HaFab extends FabBase {
protected firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
}
static override styles = [
styles,
css`
:host {
--mdc-typography-button-text-transform: none;
--mdc-typography-button-font-size: var(--ha-font-size-l);
--mdc-typography-button-font-family: var(--ha-font-family-body);
--mdc-typography-button-font-weight: var(--ha-font-weight-medium);
}
:host .mdc-fab--extended {
border-radius: var(
--ha-button-border-radius,
var(--ha-border-radius-pill)
);
}
:host .mdc-fab.mdc-fab--extended .ripple {
border-radius: var(
--ha-button-border-radius,
var(--ha-border-radius-pill)
);
}
:host .mdc-fab--extended .mdc-fab__icon {
margin-inline-start: -8px;
margin-inline-end: 12px;
direction: var(--direction);
}
:disabled {
--mdc-theme-secondary: var(--disabled-text-color);
cursor: not-allowed !important;
}
`,
// safari workaround - must be explicit
mainWindow.document.dir === "rtl"
? css`
:host .mdc-fab--extended .mdc-fab__icon {
direction: rtl;
}
`
: css``,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-fab": HaFab;
}
}

View File

@@ -1,4 +1,3 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { mdiDelete, mdiFileUpload } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
@@ -12,6 +11,7 @@ import type { HomeAssistant } from "../types";
import { bytesToString } from "../util/bytes-to-string";
import "./ha-button";
import "./ha-icon-button";
import "./progress/ha-progress-bar";
declare global {
interface HASSDomEvents {
@@ -100,10 +100,11 @@ export class HaFileUpload extends LitElement {
</div>`
: nothing}
</div>
<mwc-linear-progress
<ha-progress-bar
.indeterminate=${!this.progress}
.progress=${this.progress ? this.progress / 100 : undefined}
></mwc-linear-progress>
.value=${this.progress}
loading
></ha-progress-bar>
</div>`
: html`<label
for=${this.value ? "" : "input"}
@@ -319,7 +320,7 @@ export class HaFileUpload extends LitElement {
--mdc-button-outline-color: var(--primary-color);
--ha-icon-button-size: 24px;
}
mwc-linear-progress {
ha-progress-bar {
width: 100%;
padding: 8px 32px;
box-sizing: border-box;

View File

@@ -9,7 +9,7 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { navigate } from "../common/navigate";
import { stringCompare } from "../common/string/compare";
import { labelsContext } from "../data/context";
import { labelsContext } from "../data/context/context";
import type { LabelRegistryEntry } from "../data/label/label_registry";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";

View File

@@ -38,17 +38,6 @@ export const computeInitialHaFormData = (
// Only add expandable data if it's required or any of its children have initial values.
data[field.name] = expandableData;
}
} else if (field.type === "tabs") {
const tabsData: Record<string, unknown> = {};
for (const tab of field.tabs) {
Object.assign(tabsData, computeInitialHaFormData(tab.schema));
}
const flattenTabs = field.flatten ?? !field.name;
if (flattenTabs) {
Object.assign(data, tabsData);
} else if (field.required || Object.keys(tabsData).length) {
data[field.name] = tabsData;
}
} else if (!field.required) {
// Do nothing.
} else if (field.type === "boolean") {

View File

@@ -1,193 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import "../ha-icon";
import "../ha-svg-icon";
import "../ha-tab-group";
import "../ha-tab-group-tab";
import "./ha-form";
import type { HaForm } from "./ha-form";
import type {
HaFormDataContainer,
HaFormElement,
HaFormSchema,
HaFormTabsSchema,
} from "./types";
@customElement("ha-form-tabs")
export class HaFormTabs extends LitElement implements HaFormElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public data!: HaFormDataContainer;
@property({ attribute: false }) public schema!: HaFormTabsSchema;
@property({ type: Boolean }) public disabled = false;
@property({ attribute: false }) public computeLabel?: (
schema: HaFormSchema,
data?: HaFormDataContainer,
options?: { path?: string[]; tab?: string }
) => string;
@property({ attribute: false }) public computeHelper?: (
schema: HaFormSchema,
options?: { path?: string[] }
) => string;
@property({ attribute: false }) public localizeValue?: (
key: string
) => string;
@state() private _activeTab?: string;
private _handleTabShow = (ev: CustomEvent<{ name: string }>) => {
const name = ev.detail?.name;
if (name !== undefined) {
this._activeTab = name;
}
};
protected willUpdate(changedProps: Map<PropertyKey, unknown>): void {
super.willUpdate(changedProps);
if (changedProps.has("schema") && this.schema.tabs.length) {
const first = this.schema.tabs[0]!.name;
if (
this._activeTab === undefined ||
!this.schema.tabs.some((t) => t.name === this._activeTab)
) {
this._activeTab = first;
}
}
}
public reportValidity(): boolean {
const forms = this.renderRoot.querySelectorAll<HaForm>("ha-form");
let valid = true;
forms.forEach((form) => {
if (!form.reportValidity()) {
valid = false;
}
});
return valid;
}
private _computeLabel = (
schema: HaFormSchema,
data?: HaFormDataContainer,
options?: { path?: string[] }
) => {
if (!this.computeLabel) {
return undefined;
}
return this.computeLabel(schema, data, {
...options,
path: [...(options?.path || []), this.schema.name],
});
};
private _computeHelper = (
schema: HaFormSchema,
options?: { path?: string[] }
) => {
if (!this.computeHelper) {
return undefined;
}
return this.computeHelper(schema, {
...options,
path: [...(options?.path || []), this.schema.name],
});
};
private _tabTitle(tabName: string): string {
if (!this.computeLabel) {
return tabName;
}
return (
this.computeLabel(this.schema, this.data, {
path: [...(this.schema.name ? [this.schema.name] : [])],
tab: tabName,
}) ?? tabName
);
}
protected render() {
const tabs = this.schema.tabs;
if (!tabs.length) {
return nothing;
}
const active = this._activeTab ?? tabs[0]!.name;
const fillTabs = this.schema.fill_tabs !== false;
return html`
<ha-tab-group ?fill-tabs=${fillTabs} @wa-tab-show=${this._handleTabShow}>
${tabs.map(
(tab) => html`
<ha-tab-group-tab
slot="nav"
.panel=${tab.name}
.active=${active === tab.name}
>
${tab.icon
? html`<ha-icon .icon=${tab.icon}></ha-icon>`
: tab.iconPath
? html`<ha-svg-icon .path=${tab.iconPath}></ha-svg-icon>`
: nothing}
${this._tabTitle(tab.name)}
</ha-tab-group-tab>
`
)}
</ha-tab-group>
<div class="panels">
${tabs.map((tab) => {
const hidden = active !== tab.name;
return html`
<div class="panel" ?hidden=${hidden}>
<ha-form
.hass=${this.hass}
.data=${this.data}
.schema=${tab.schema}
.disabled=${this.disabled}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
.localizeValue=${this.localizeValue}
></ha-form>
</div>
`;
})}
</div>
`;
}
static styles = css`
:host {
display: flex !important;
flex-direction: column;
}
.panels {
padding-top: var(--ha-space-4);
}
.panel[hidden] {
display: none !important;
}
:host ha-form {
display: block;
}
ha-tab-group {
display: block;
}
ha-tab-group-tab ha-icon,
ha-tab-group-tab ha-svg-icon {
flex-shrink: 0;
margin-inline-end: var(--ha-space-2);
color: var(--secondary-text-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-tabs": HaFormTabs;
}
}

View File

@@ -14,7 +14,6 @@ const LOAD_ELEMENTS = {
float: () => import("./ha-form-float"),
grid: () => import("./ha-form-grid"),
expandable: () => import("./ha-form-expandable"),
tabs: () => import("./ha-form-tabs"),
integer: () => import("./ha-form-integer"),
multi_select: () => import("./ha-form-multi_select"),
positive_time_period_dict: () =>

View File

@@ -14,8 +14,7 @@ export type HaFormSchema =
| HaFormSelector
| HaFormGridSchema
| HaFormExpandableSchema
| HaFormOptionalActionsSchema
| HaFormTabsSchema;
| HaFormOptionalActionsSchema;
export interface HaFormBaseSchema {
name: string;
@@ -55,26 +54,6 @@ export interface HaFormOptionalActionsSchema extends HaFormBaseSchema {
schema: readonly HaFormSchema[];
}
/** One tab pane inside a {@link HaFormTabsSchema} (not a standalone form field). */
export interface HaFormTabDefinition {
name: string;
icon?: string;
iconPath?: string;
schema: readonly HaFormSchema[];
}
export interface HaFormTabsSchema extends HaFormBaseSchema {
type: "tabs";
/** When true (default), tab field values merge into the parent data object. */
flatten?: boolean;
/**
* When true (default), tab labels share width equally across the tab bar.
* Set to false for compact tabs that only use their natural width.
*/
fill_tabs?: boolean;
tabs: readonly HaFormTabDefinition[];
}
export interface HaFormSelector extends HaFormBaseSchema {
type?: never;
selector: Selector;
@@ -125,13 +104,6 @@ export interface HaFormTimeSchema extends HaFormBaseSchema {
}
// Type utility to unionize a schema array by flattening any grid schemas
type SchemaUnionTabs<T extends readonly HaFormTabDefinition[]> =
T[number] extends infer Tab
? Tab extends HaFormTabDefinition
? SchemaUnion<Tab["schema"]>
: never
: never;
export type SchemaUnion<
SchemaArray extends readonly HaFormSchema[],
Schema = SchemaArray[number],
@@ -140,9 +112,7 @@ export type SchemaUnion<
| HaFormExpandableSchema
| HaFormOptionalActionsSchema
? SchemaUnion<Schema["schema"]> | Schema
: Schema extends HaFormTabsSchema
? SchemaUnionTabs<Schema["tabs"]> | Schema
: Schema;
: Schema;
export type HaFormDataContainer = Record<string, HaFormData>;

View File

@@ -15,7 +15,7 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import { labelsContext } from "../data/context";
import { labelsContext } from "../data/context/context";
import {
getLabels,
labelComboBoxKeys,

View File

@@ -8,7 +8,7 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import { labelsContext } from "../data/context";
import { labelsContext } from "../data/context/context";
import type { LabelRegistryEntry } from "../data/label/label_registry";
import { updateLabelRegistryEntry } from "../data/label/label_registry";
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";

View File

@@ -15,7 +15,7 @@ import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { localeContext, localizeContext } from "../data/context";
import { internationalizationContext } from "../data/context/context";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import {
multiTermSortedSearch,
@@ -162,12 +162,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@query("ha-input-search") private _searchFieldElement?: HaInputSearch;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@state()
@consume({ context: localeContext, subscribe: true })
private locale!: ContextType<typeof localeContext>;
@consume({ context: internationalizationContext, subscribe: true })
private i18n!: ContextType<typeof internationalizationContext>;
@state() private _items: PickerComboBoxItem[] = [];
@@ -222,9 +218,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
const searchLabel =
this.label ??
(this.allowCustomValue
? (this.localize?.("ui.components.combo-box.search_or_custom") ??
? (this.i18n.localize?.("ui.components.combo-box.search_or_custom") ??
"Search | Add custom value")
: (this.localize?.("ui.common.search") ?? "Search"));
: (this.i18n.localize?.("ui.common.search") ?? "Search"));
return html`<ha-input-search
appearance="outlined"
@@ -351,7 +347,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
return caseInsensitiveStringCompare(
sortLabelA,
sortLabelB,
this.locale?.language ?? navigator.language
this.i18n.locale?.language ?? navigator.language
);
});
}
@@ -368,7 +364,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
id: this._search,
primary:
this.customValueLabel ??
this.localize?.("ui.components.combo-box.add_custom_item") ??
this.i18n.localize?.("ui.components.combo-box.add_custom_item") ??
"Add custom item",
secondary: `"${this._search}"`,
icon_path: mdiPlus,
@@ -402,10 +398,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
? typeof this.notFoundLabel === "function"
? this.notFoundLabel(this._search)
: this.notFoundLabel ||
this.localize?.("ui.components.combo-box.no_match") ||
this.i18n.localize?.("ui.components.combo-box.no_match") ||
"No matching items found"
: this.emptyLabel ||
this.localize?.("ui.components.combo-box.no_items") ||
this.i18n.localize?.("ui.components.combo-box.no_items") ||
"No items available"}</span
>
</ha-combo-box-item>
@@ -497,7 +493,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
id: searchString,
primary:
this.customValueLabel ??
this.localize?.("ui.components.combo-box.add_custom_item") ??
this.i18n.localize?.("ui.components.combo-box.add_custom_item") ??
"Add custom item",
secondary: `"${searchString}"`,
icon_path: mdiPlus,

View File

@@ -1,4 +1,4 @@
import { consume } from "@lit/context";
import { consume, type ContextType } from "@lit/context";
import { mdiClose, mdiMenuDown } from "@mdi/js";
import {
css,
@@ -11,9 +11,8 @@ import {
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { localizeContext } from "../data/context";
import { internationalizationContext } from "../data/context/context";
import { PickerMixin } from "../mixins/picker-mixin";
import type { HomeAssistant } from "../types";
import "./ha-combo-box-item";
import type { HaComboBoxItem } from "./ha-combo-box-item";
import "./ha-icon";
@@ -34,8 +33,8 @@ export class HaPickerField extends PickerMixin(LitElement) {
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: HomeAssistant["localize"];
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
public async focus() {
await this.updateComplete;
@@ -89,7 +88,7 @@ export class HaPickerField extends PickerMixin(LitElement) {
${this.unknown
? html`<div slot="supporting-text" class="unknown">
${this.unknownItemText ||
this.localize("ui.components.combo-box.unknown_item")}
this._i18n?.localize("ui.components.combo-box.unknown_item")}
</div>`
: nothing}
${showClearIcon

View File

@@ -2,7 +2,7 @@ import { consume } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fullEntitiesContext } from "../../data/context";
import { fullEntitiesContext } from "../../data/context/context";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { Action } from "../../data/script";
import { migrateAutomationAction } from "../../data/script";

View File

@@ -23,7 +23,10 @@ export class HaSlider extends Slider {
--marker-height: calc(var(--ha-slider-track-size, 4px) / 2);
--marker-width: calc(var(--ha-slider-track-size, 4px) / 2);
--wa-color-surface-default: var(--card-background-color);
--wa-color-neutral-fill-normal: var(--disabled-color);
--wa-color-neutral-fill-normal: var(
--ha-slider-track-color,
var(--disabled-color)
);
--wa-tooltip-background-color: var(
--ha-tooltip-background-color,
var(--secondary-background-color)

View File

@@ -26,11 +26,6 @@ export class HaTabGroupTab extends Tab {
opacity: 1;
}
.tab {
width: var(--ha-tab-base-width, auto);
justify-content: var(--ha-tab-base-justify-content, flex-start);
}
@media (hover: hover) {
:host(:hover:not([disabled]):not([active])) .tab {
color: var(--wa-color-brand-on-quiet);

View File

@@ -13,10 +13,6 @@ export class HaTabGroup extends TabGroup {
@property({ attribute: "tab-only", type: Boolean }) tabOnly = true;
/** When true (default), each tab trigger grows to fill the tab row evenly. */
@property({ type: Boolean, reflect: true, attribute: "fill-tabs" })
fillTabs = true;
connectedCallback(): void {
super.connectedCallback();
// Prevent the tab group from consuming Alt+Arrow and Cmd+Arrow keys,
@@ -74,13 +70,6 @@ export class HaTabGroup extends TabGroup {
.scroll-button::part(base):hover {
background-color: transparent;
}
:host([fill-tabs]) .tab-group-top .tabs ::slotted(ha-tab-group-tab),
:host([fill-tabs]) .tab-group-bottom .tabs ::slotted(ha-tab-group-tab) {
flex: 1;
--ha-tab-base-width: 100%;
--ha-tab-base-justify-content: center;
}
`,
];
}

View File

@@ -23,7 +23,7 @@ import {
type FloorComboBoxItem,
} from "../data/area_floor_picker";
import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
import { labelsContext } from "../data/context";
import { labelsContext } from "../data/context/context";
import {
deviceComboBoxKeys,
getDevices,

View File

@@ -3,7 +3,7 @@ import { mdiContentCopy, mdiEye, mdiEyeOff } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { copyToClipboard } from "../../common/util/copy-clipboard";
import { localizeContext } from "../../data/context";
import { internationalizationContext } from "../../data/context/context";
import { showToast } from "../../util/toast";
import "../ha-button";
import "../ha-icon-button";
@@ -59,8 +59,8 @@ export class HaInputCopy extends LitElement {
false;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state() private _showMasked = true;
@@ -90,7 +90,7 @@ export class HaInputCopy extends LitElement {
? html`<ha-icon-button
slot="end"
class="toggle-unmasked"
.label=${this.localize(
.label=${this._i18n.localize(
`ui.common.${this._showMasked ? "show" : "hide"}`
)}
@click=${this._toggleMasked}
@@ -101,7 +101,7 @@ export class HaInputCopy extends LitElement {
</div>
<ha-button @click=${this._copy} appearance="plain" size="small">
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
${this.label || this.localize("ui.common.copy")}
${this.label || this._i18n.localize("ui.common.copy")}
</ha-button>
</div>
`;
@@ -119,7 +119,7 @@ export class HaInputCopy extends LitElement {
private async _copy(): Promise<void> {
await copyToClipboard(this.value);
showToast(this, {
message: this.localize("ui.common.copied_clipboard"),
message: this._i18n.localize("ui.common.copied_clipboard"),
});
}

View File

@@ -5,7 +5,7 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../common/dom/fire_event";
import { localizeContext } from "../../data/context";
import { internationalizationContext } from "../../data/context/context";
import { haStyle } from "../../resources/styles";
import "../ha-button";
import "../ha-icon-button";
@@ -64,8 +64,8 @@ class HaInputMulti extends LitElement {
public updateOnBlur = false;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize?: ContextType<typeof localizeContext>;
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
protected render() {
return html`
@@ -109,7 +109,7 @@ class HaInputMulti extends LitElement {
.index=${index}
slot="navigationIcon"
.label=${this.removeLabel ??
this.localize?.("ui.common.remove") ??
this._i18n?.localize("ui.common.remove") ??
"Remove"}
@click=${this._removeItem}
.path=${mdiDeleteOutline}
@@ -137,10 +137,10 @@ class HaInputMulti extends LitElement {
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.addLabel ??
(this.label
? this.localize?.("ui.components.multi-textfield.add_item", {
? this._i18n?.localize("ui.components.multi-textfield.add_item", {
item: this.label,
})
: this.localize?.("ui.common.add")) ??
: this._i18n?.localize("ui.common.add")) ??
"Add"}
</ha-button>
</div>

View File

@@ -2,7 +2,7 @@ import { consume, type ContextType } from "@lit/context";
import { mdiMagnify } from "@mdi/js";
import { html, type PropertyValues } from "lit";
import { customElement, state } from "lit/decorators";
import { localizeContext } from "../../data/context";
import { internationalizationContext } from "../../data/context/context";
import { HaInput } from "./ha-input";
/**
@@ -18,8 +18,8 @@ import { HaInput } from "./ha-input";
@customElement("ha-input-search")
export class HaInputSearch extends HaInput {
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
constructor() {
super();
@@ -33,9 +33,9 @@ export class HaInputSearch extends HaInput {
if (
!this.label &&
!this.placeholder &&
(!this.hasUpdated || changedProps.has("localize"))
(!this.hasUpdated || changedProps.has("_i18n"))
) {
this.placeholder = this.localize("ui.common.search");
this.placeholder = this._i18n.localize("ui.common.search");
}
}

View File

@@ -254,7 +254,11 @@ export class HaMap extends ReactiveElement {
}
this._loading = true;
try {
[this.leafletMap, this.Leaflet] = await setupLeafletMap(map);
[this.leafletMap, this.Leaflet] = await setupLeafletMap(map, {
latitude: this.hass?.config.latitude ?? 52.3731339,
longitude: this.hass?.config.longitude ?? 4.8903147,
zoom: this.zoom,
});
this._updateMapStyle();
this.leafletMap.on("click", (ev) => {
if (this._clickCount === 0) {

View File

@@ -79,6 +79,7 @@ class DialogMediaPlayerBrowse extends LitElement {
<ha-dialog
.hass=${this.hass}
.open=${this._open}
width="large"
flexcontent
@closed=${this.closeDialog}
@opened=${this._dialogOpened}
@@ -230,6 +231,8 @@ class DialogMediaPlayerBrowse extends LitElement {
--media-browser-max-height: calc(
100vh - 65px - var(--safe-area-inset-y)
);
height: 100vh;
height: 100dvh;
}
:host(.opened) ha-media-player-browse {
@@ -248,7 +251,6 @@ class DialogMediaPlayerBrowse extends LitElement {
--media-browser-max-height: calc(
100vh - 145px - var(--safe-area-inset-y)
);
width: 700px;
}
}

View File

@@ -49,7 +49,6 @@ import "../entity/ha-entity-picker";
import "../ha-alert";
import "../ha-button";
import "../ha-card";
import "../ha-fab";
import "../ha-icon-button";
import "../ha-list";
import "../ha-list-item";
@@ -446,24 +445,20 @@ export class HaMediaPlayerBrowse extends LitElement {
currentItem.media_content_id
))
? html`
<ha-fab
mini
<ha-button
class="fab"
.item=${currentItem}
@click=${this._actionClicked}
.title=${this.hass.localize(
`ui.components.media-browser.${this.action}`
)}
>
<ha-svg-icon
slot="icon"
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
></ha-svg-icon>
${this.hass.localize(
`ui.components.media-browser.${this.action}`
)}
</ha-fab>
</ha-button>
`
: ""}
</div>
@@ -1363,11 +1358,11 @@ export class HaMediaPlayerBrowse extends LitElement {
height 0.4s,
padding-bottom 0.4s;
}
ha-fab {
.fab {
position: absolute;
--mdc-theme-secondary: var(--primary-color);
bottom: -20px;
right: 20px;
bottom: calc(var(--ha-space-5) * -1);
right: var(--ha-space-5);
--ha-button-box-shadow: var(--ha-box-shadow-l);
}
:host([narrow]) .header-info ha-button {
margin-top: 16px;
@@ -1429,11 +1424,10 @@ export class HaMediaPlayerBrowse extends LitElement {
padding-bottom: initial;
margin-bottom: 0;
}
:host([scrolled]) ha-fab {
bottom: 0px;
right: -24px;
--mdc-fab-box-shadow: none;
--mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5);
:host([scrolled]) .fab {
bottom: 0;
right: calc(var(--ha-space-6) * -1);
--ha-button-box-shadow: none;
}
lit-virtualizer {

View File

@@ -0,0 +1,92 @@
import ProgressBar from "@home-assistant/webawesome/dist/components/progress-bar/progress-bar";
import type { CSSResultGroup } from "lit";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
/**
* Home Assistant progress bar component
*
* @element ha-progress-bar
* @extends {ProgressBar}
*
* @summary
* A stylable progress bar component based on webawesome progress bar.
* Supports regular, indeterminate, and loading states with Home Assistant theming.
*
* @cssprop --ha-progress-bar-indicator-color - Color of the filled progress indicator.
* @cssprop --ha-progress-bar-track-color - Color of the progress track.
* @cssprop --ha-progress-bar-track-height - Height of the progress track. Defaults to `16px`.
* @cssprop --ha-progress-bar-border-radius - Border radius of the progress bar. Defaults to `var(--ha-border-radius-pill)`.
* @cssprop --ha-progress-bar-animation-duration - Animation duration for indeterminate/loading highlight. Defaults to `2.5s`.
*
* @attr {boolean} loading - Shows the loading highlight animation on top of the indicator.
* @attr {boolean} indeterminate - Shows indeterminate progress animation (inherited from ProgressBar).
*/
@customElement("ha-progress-bar")
export class HaProgressBar extends ProgressBar {
@property({ type: Boolean, reflect: true })
loading = false;
static get styles(): CSSResultGroup {
return [
ProgressBar.styles,
css`
:host {
--indicator-color: var(
--ha-progress-bar-indicator-color,
var(--ha-color-on-primary-normal)
);
--track-color: var(
--ha-progress-bar-track-color,
var(--ha-color-fill-neutral-normal-hover)
);
--track-height: var(--ha-progress-bar-track-height, 16px);
--wa-transition-slow: var(--ha-animation-duration-slow);
}
.progress-bar {
border-radius: var(
--ha-progress-bar-border-radius,
var(--ha-border-radius-pill)
);
}
@keyframes slide-highlight {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
:host([indeterminate]) .indicator {
animation: wa-progress-indeterminate
var(--ha-progress-bar-animation-duration, 2.5s) infinite
cubic-bezier(0.37, 0, 0.63, 1);
}
:host([indeterminate]) .indicator::after,
:host([loading]) .indicator::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent 0%,
var(--ha-color-fill-primary-normal-hover) 45%,
var(--ha-color-fill-primary-normal-active) 50%,
var(--ha-color-fill-primary-normal-hover) 55%,
transparent 100%
);
opacity: 0.4;
animation: slide-highlight
var(--ha-progress-bar-animation-duration, 2.5s) infinite
cubic-bezier(0.37, 0, 0.63, 1);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-progress-bar": HaProgressBar;
}
}

View File

@@ -23,7 +23,7 @@ import { getEntityContext } from "../../common/entity/context/get_entity_context
import { computeRTL } from "../../common/util/compute_rtl";
import type { AreaRegistryEntry } from "../../data/area/area_registry";
import { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context";
import { labelsContext } from "../../data/context/context";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
import type { FloorRegistryEntry } from "../../data/floor_registry";

View File

@@ -21,7 +21,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { slugify } from "../../common/string/slugify";
import { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context";
import { labelsContext } from "../../data/context/context";
import { domainToName } from "../../data/integration";
import type { LabelRegistryEntry } from "../../data/label/label_registry";
import type { TargetType } from "../../data/target";

View File

@@ -5,8 +5,10 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import type { Trigger } from "../../data/automation";
import { migrateAutomationTrigger } from "../../data/automation";
import { describeCondition, describeTrigger } from "../../data/automation_i18n";
import { fullEntitiesContext, labelsContext } from "../../data/context";
import { fullEntitiesContext, labelsContext } from "../../data/context/context";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { LabelRegistryEntry } from "../../data/label/label_registry";
import type { LogbookEntry } from "../../data/logbook";
@@ -24,8 +26,6 @@ import "../ha-icon-button";
import "./hat-logbook-note";
import type { NodeInfo } from "./hat-script-graph";
import { traceTabStyles } from "./trace-tab-styles";
import type { Trigger } from "../../data/automation";
import { migrateAutomationTrigger } from "../../data/automation";
const TRACE_PATH_TABS = [
"step_config",

View File

@@ -15,7 +15,7 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
import { relativeTime } from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event";
import { toggleAttribute } from "../../common/dom/toggle_attribute";
import { fullEntitiesContext } from "../../data/context";
import { fullEntitiesContext } from "../../data/context/context";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { LogbookEntry } from "../../data/logbook";
import type {

View File

@@ -1,3 +1,4 @@
import { TZDate } from "@date-fns/tz";
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
import { ensureArray } from "../common/array/ensure-array";
import {
@@ -68,8 +69,22 @@ const localizeTimeString = (
return time;
}
try {
const dt = new Date("1970-01-01T" + time);
if (chunks.length === 2 || Number(chunks[2]) === 0) {
const hours = Number(chunks[0]);
const minutes = Number(chunks[1]);
const seconds = chunks.length > 2 ? Number(chunks[2]) : 0;
// Create date in the server timezone so formatTime converts correctly
// when the user's browser timezone differs from the HA server timezone.
const now = new Date();
const dt = new TZDate(
now.getFullYear(),
now.getMonth(),
now.getDate(),
hours,
minutes,
seconds,
config.time_zone
);
if (chunks.length === 2 || seconds === 0) {
return formatTime(dt, locale, config);
}
return formatTimeWithSeconds(dt, locale, config);
@@ -1197,7 +1212,17 @@ const describeLegacyCondition = (
let hasTime = "";
if (after !== undefined && before !== undefined) {
hasTime = "after_before";
if (
typeof condition.after === "string" &&
!condition.after.includes(".") &&
typeof condition.before === "string" &&
!condition.before.includes(".") &&
condition.after > condition.before
) {
hasTime = "after_before_or";
} else {
hasTime = "after_before";
}
} else if (after !== undefined) {
hasTime = "after";
} else if (before !== undefined) {

View File

@@ -1,38 +0,0 @@
import { createContext } from "@lit/context";
import type { HassConfig } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import type { ConfigEntry } from "./config_entries";
import type { EntityRegistryEntry } from "./entity/entity_registry";
import type { LabelRegistryEntry } from "./label/label_registry";
export const connectionContext =
createContext<HomeAssistant["connection"]>("connection");
export const statesContext = createContext<HomeAssistant["states"]>("states");
export const entitiesContext =
createContext<HomeAssistant["entities"]>("entities");
export const devicesContext =
createContext<HomeAssistant["devices"]>("devices");
export const areasContext = createContext<HomeAssistant["areas"]>("areas");
export const localizeContext =
createContext<HomeAssistant["localize"]>("localize");
export const localeContext = createContext<HomeAssistant["locale"]>("locale");
export const configContext = createContext<HassConfig>("config");
export const themesContext = createContext<HomeAssistant["themes"]>("themes");
export const selectedThemeContext =
createContext<HomeAssistant["selectedTheme"]>("selectedTheme");
export const userContext = createContext<HomeAssistant["user"]>("user");
export const userDataContext =
createContext<HomeAssistant["userData"]>("userData");
export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
export const fullEntitiesContext =
createContext<EntityRegistryEntry[]>("extendedEntities");
export const floorsContext = createContext<HomeAssistant["floors"]>("floors");
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
export const configEntriesContext =
createContext<ConfigEntry[]>("configEntries");
export const authContext = createContext<HomeAssistant["auth"]>("auth");

148
src/data/context/context.ts Normal file
View File

@@ -0,0 +1,148 @@
import { createContext } from "@lit/context";
import type { HassConfig } from "home-assistant-js-websocket";
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantConfig,
HomeAssistantConnection,
HomeAssistantInternationalization,
HomeAssistantRegistries,
HomeAssistantUI,
} from "../../types";
import type { ConfigEntry } from "../config_entries";
import type { EntityRegistryEntry } from "../entity/entity_registry";
import type { LabelRegistryEntry } from "../label/label_registry";
/**
* Entity, device, area, and floor registries
*/
export const registriesContext =
createContext<HomeAssistantRegistries>("hassRegistries");
/**
* Live map of all entity states, keyed by entity ID.
*/
export const statesContext = createContext<HomeAssistant["states"]>("states");
/**
* Provides the map of all available Home Assistant services, keyed by domain.
*/
export const servicesContext =
createContext<HomeAssistant["services"]>("services");
/**
* i18n state: active language, locale settings, the `localize` function, translation metadata, and the
* `loadBackendTranslation` / `loadFragmentTranslation` loaders.
*/
export const internationalizationContext =
createContext<HomeAssistantInternationalization>("hassInternationalization");
/**
* HTTP and WebSocket API surface: `callService`, `callApi`,
* `callApiRaw`, `callWS`, `sendWS`, `fetchWithAuth`, and `hassUrl`.
*/
export const apiContext = createContext<HomeAssistantApi>("hassApi");
/**
* WebSocket connection state: `connection`, `connected`, and `debugConnection`.
*/
export const connectionContext =
createContext<HomeAssistantConnection>("hassConnection");
/**
* UI preferences and global UI state: themes, selected theme,
* panels, sidebar mode, kiosk mode, shortcuts, vibration, and
* `suspendWhenHidden`.
*/
export const uiContext = createContext<HomeAssistantUI>("hassUi");
/**
* HA core configuration together with user session data:
* `auth`, `config` (core HA config), `user`, `userData`, and `systemData`.
*/
export const configContext = createContext<HomeAssistantConfig>("hassConfig");
/**
* Map of all entities in the entity registry, keyed by entity ID.
*/
export const entitiesContext =
createContext<HomeAssistant["entities"]>("entities");
/**
* Map of all devices in the device registry, keyed by device ID.
*/
export const devicesContext =
createContext<HomeAssistant["devices"]>("devices");
/**
* Map of all areas in the area registry, keyed by area ID.
*/
export const areasContext = createContext<HomeAssistant["areas"]>("areas");
/**
* Map of all floors in the floor registry, keyed by floor ID.
*/
export const floorsContext = createContext<HomeAssistant["floors"]>("floors");
// #region lazy-contexts
/**
* Lazy contexts are not subscribed to by default. They are only subscribed to when a provider is consumed with at least one consumer.
*/
/**
* Lazy loaded labels registry, keyed by label ID.
*/
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
/**
* Lazy loaded entity registry array
*/
export const fullEntitiesContext =
createContext<EntityRegistryEntry[]>("extendedEntities");
/**
* Lazy loaded config entries array
*/
export const configEntriesContext =
createContext<ConfigEntry[]>("configEntries");
// #endregion lazy-contexts
// #region deprecated-contexts
/** @deprecated Use `connectionContext` instead. */
export const connectionSingleContext =
createContext<HomeAssistant["connection"]>("connection");
/** @deprecated Use `internationalizationContext` instead. */
export const localizeContext =
createContext<HomeAssistant["localize"]>("localize");
/** @deprecated Use `internationalizationContext` instead. */
export const localeContext = createContext<HomeAssistant["locale"]>("locale");
/** @deprecated Use `configContext` instead. */
export const configSingleContext = createContext<HassConfig>("config");
/** @deprecated Use `uiContext` instead. */
export const themesContext = createContext<HomeAssistant["themes"]>("themes");
/** @deprecated Use `uiContext` instead. */
export const selectedThemeContext =
createContext<HomeAssistant["selectedTheme"]>("selectedTheme");
/** @deprecated Use `configContext` instead. */
export const userContext = createContext<HomeAssistant["user"]>("user");
/** @deprecated Use `configContext` instead. */
export const userDataContext =
createContext<HomeAssistant["userData"]>("userData");
/** @deprecated Use `uiContext` instead. */
export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
/** @deprecated Use `configContext` instead. */
export const authContext = createContext<HomeAssistant["auth"]>("auth");
// #endregion deprecated-contexts

View File

@@ -0,0 +1,166 @@
import type {
HomeAssistant,
HomeAssistantApi,
HomeAssistantConfig,
HomeAssistantConnection,
HomeAssistantInternationalization,
HomeAssistantRegistries,
HomeAssistantUI,
} from "../../types";
const updateRegistries = (
hass: HomeAssistant,
value?: HomeAssistantRegistries
): HomeAssistantRegistries => {
if (
!value ||
value.entities !== hass.entities ||
value.devices !== hass.devices ||
value.areas !== hass.areas ||
value.floors !== hass.floors
) {
return {
entities: hass.entities,
devices: hass.devices,
areas: hass.areas,
floors: hass.floors,
};
}
return value;
};
const updateInternationalization = (
hass: HomeAssistant,
value?: HomeAssistantInternationalization
): HomeAssistantInternationalization => {
if (
!value ||
value.localize !== hass.localize ||
value.locale !== hass.locale ||
value.loadBackendTranslation !== hass.loadBackendTranslation ||
value.loadFragmentTranslation !== hass.loadFragmentTranslation ||
value.language !== hass.language ||
value.selectedLanguage !== hass.selectedLanguage ||
value.translationMetadata !== hass.translationMetadata
) {
return {
localize: hass.localize,
locale: hass.locale,
loadBackendTranslation: hass.loadBackendTranslation,
loadFragmentTranslation: hass.loadFragmentTranslation,
language: hass.language,
selectedLanguage: hass.selectedLanguage,
translationMetadata: hass.translationMetadata,
};
}
return value;
};
const updateApi = (
hass: HomeAssistant,
value?: HomeAssistantApi
): HomeAssistantApi => {
if (
!value ||
value.callService !== hass.callService ||
value.callApi !== hass.callApi ||
value.callApiRaw !== hass.callApiRaw ||
value.callWS !== hass.callWS ||
value.sendWS !== hass.sendWS ||
value.fetchWithAuth !== hass.fetchWithAuth ||
value.hassUrl !== hass.hassUrl
) {
return {
callService: hass.callService,
callApi: hass.callApi,
callApiRaw: hass.callApiRaw,
callWS: hass.callWS,
sendWS: hass.sendWS,
fetchWithAuth: hass.fetchWithAuth,
hassUrl: hass.hassUrl,
};
}
return value;
};
const updateConnection = (
hass: HomeAssistant,
value?: HomeAssistantConnection
): HomeAssistantConnection => {
if (
!value ||
value.connection !== hass.connection ||
value.connected !== hass.connected ||
value.debugConnection !== hass.debugConnection
) {
return {
connection: hass.connection,
connected: hass.connected,
debugConnection: hass.debugConnection,
};
}
return value;
};
const updateUi = (
hass: HomeAssistant,
value?: HomeAssistantUI
): HomeAssistantUI => {
if (
!value ||
value.themes !== hass.themes ||
value.selectedTheme !== hass.selectedTheme ||
value.panels !== hass.panels ||
value.panelUrl !== hass.panelUrl ||
value.dockedSidebar !== hass.dockedSidebar ||
value.kioskMode !== hass.kioskMode ||
value.enableShortcuts !== hass.enableShortcuts ||
value.vibrate !== hass.vibrate ||
value.suspendWhenHidden !== hass.suspendWhenHidden
) {
return {
themes: hass.themes,
selectedTheme: hass.selectedTheme,
panels: hass.panels,
panelUrl: hass.panelUrl,
dockedSidebar: hass.dockedSidebar,
kioskMode: hass.kioskMode,
enableShortcuts: hass.enableShortcuts,
vibrate: hass.vibrate,
suspendWhenHidden: hass.suspendWhenHidden,
};
}
return value;
};
const updateConfig = (
hass: HomeAssistant,
value?: HomeAssistantConfig
): HomeAssistantConfig => {
if (
!value ||
value.auth !== hass.auth ||
value.config !== hass.config ||
value.user !== hass.user ||
value.userData !== hass.userData ||
value.systemData !== hass.systemData
) {
return {
auth: hass.auth,
config: hass.config,
user: hass.user,
userData: hass.userData,
systemData: hass.systemData,
};
}
return value;
};
export const updateHassGroups = {
registries: updateRegistries,
internationalization: updateInternationalization,
api: updateApi,
connection: updateConnection,
ui: updateUi,
config: updateConfig,
};

View File

@@ -4,6 +4,7 @@ export interface CoreFrontendUserData {
showAdvanced?: boolean;
showEntityIdPicker?: boolean;
default_panel?: string;
apps_info_dismissed?: boolean;
}
export interface SidebarFrontendUserData {

View File

@@ -105,10 +105,6 @@ const generateNavigationConfigSectionCommands = (
hass: HomeAssistant,
filterOptions: NavigationFilterOptions = {}
): BaseNavigationCommand[] => {
if (!hass.user?.is_admin) {
return [];
}
const items: NavigationInfo[] = [];
const allPages = Object.values(configSections).flat();
const visiblePages = filterNavigationPages(hass, allPages, filterOptions);

View File

@@ -99,16 +99,6 @@ export const showConfigFlowDialog = (
);
}
if (field.type === "tabs" && options?.tab) {
const sectionPrefix = field.name ? `sections.${field.name}.` : "";
return (
hass.localize(
`component.${step.handler}.config.step.${step.step_id}.${sectionPrefix}tabs.${options.tab}.name`,
step.description_placeholders
) || options.tab
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
return (
@@ -127,17 +117,6 @@ export const showConfigFlowDialog = (
);
}
if (field.type === "tabs" && options?.tab) {
const sectionPrefix = field.name ? `sections.${field.name}.` : "";
const tabDescription = hass.localize(
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.${sectionPrefix}tabs.${options.tab}.description`,
step.description_placeholders
);
return tabDescription
? html`<ha-markdown breaks .content=${tabDescription}></ha-markdown>`
: "";
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
const description = hass.localize(

View File

@@ -62,14 +62,14 @@ export interface FlowConfig {
hass: HomeAssistant,
step: DataEntryFlowStepForm,
field: HaFormSchema,
options: { path?: string[]; tab?: string; [key: string]: any }
options: { path?: string[]; [key: string]: any }
): string;
renderShowFormStepFieldHelper(
hass: HomeAssistant,
step: DataEntryFlowStepForm,
field: HaFormSchema,
options: { path?: string[]; tab?: string; [key: string]: any }
options: { path?: string[]; [key: string]: any }
): TemplateResult | string;
renderShowFormStepFieldError(

View File

@@ -103,16 +103,6 @@ export const showOptionsFlowDialog = (
);
}
if (field.type === "tabs" && options?.tab) {
const sectionPrefix = field.name ? `sections.${field.name}.` : "";
return (
hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.${sectionPrefix}tabs.${options.tab}.name`,
step.description_placeholders
) || options.tab
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
return (
@@ -131,20 +121,6 @@ export const showOptionsFlowDialog = (
);
}
if (field.type === "tabs" && options?.tab) {
const sectionPrefix = field.name ? `sections.${field.name}.` : "";
const tabDescription = hass.localize(
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.${sectionPrefix}tabs.${options.tab}.description`,
step.description_placeholders
);
return tabDescription
? html`<ha-markdown
breaks
.content=${tabDescription}
></ha-markdown>`
: "";
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
const description = hass.localize(

View File

@@ -95,16 +95,6 @@ export const showSubConfigFlowDialog = (
);
}
if (field.type === "tabs" && options?.tab) {
const sectionPrefix = field.name ? `sections.${field.name}.` : "";
return (
hass.localize(
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${sectionPrefix}tabs.${options.tab}.name`,
step.description_placeholders
) || options.tab
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
return (
@@ -123,17 +113,6 @@ export const showSubConfigFlowDialog = (
);
}
if (field.type === "tabs" && options?.tab) {
const sectionPrefix = field.name ? `sections.${field.name}.` : "";
const tabDescription = hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${sectionPrefix}tabs.${options.tab}.description`,
step.description_placeholders
);
return tabDescription
? html`<ha-markdown breaks .content=${tabDescription}></ha-markdown>`
: "";
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
const description = hass.localize(

View File

@@ -75,17 +75,7 @@ class StepFlowForm extends LitElement {
handleReadOnlyField(sectionField)
),
}
: field.type === "tabs" && field.tabs
? {
...field,
tabs: field.tabs.map((tab) => ({
...tab,
schema: tab.schema.map((tabField) =>
handleReadOnlyField(tabField)
),
})),
}
: handleReadOnlyField(field)
: handleReadOnlyField(field)
);
});

View File

@@ -2,8 +2,8 @@ import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { blankBeforePercent } from "../../common/translations/blank_before_percent";
import "../../components/ha-progress-ring";
import "../../components/ha-spinner";
import "../../components/progress/ha-progress-ring";
import type { DataEntryFlowStepProgress } from "../../data/data_entry_flow";
import type { HomeAssistant } from "../../types";
import type { FlowConfig } from "./show-dialog-data-entry-flow";

View File

@@ -1,4 +1,3 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
@@ -15,6 +14,7 @@ import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-switch";
import "../../../components/progress/ha-progress-bar";
import type { BackupConfig } from "../../../data/backup";
import { fetchBackupConfig } from "../../../data/backup";
import { isUnavailableState } from "../../../data/entity/entity";
@@ -191,11 +191,11 @@ class MoreInfoUpdate extends LitElement {
${this.stateObj.attributes.in_progress
? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
this.stateObj.attributes.update_percentage !== null
? html`<mwc-linear-progress
.progress=${this.stateObj.attributes.update_percentage / 100}
buffer=""
></mwc-linear-progress>`
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
? html`<ha-progress-bar
loading
.value=${this.stateObj.attributes.update_percentage}
></ha-progress-bar>`
: html`<ha-progress-bar indeterminate></ha-progress-bar>`
: nothing}
<h3>${this.stateObj.attributes.title}</h3>
${this._error
@@ -521,10 +521,6 @@ class MoreInfoUpdate extends LitElement {
justify-content: center;
align-items: center;
}
mwc-linear-progress {
margin-bottom: calc(var(--ha-space-2) * -1);
margin-top: var(--ha-space-1);
}
ha-markdown {
direction: ltr;
padding-bottom: var(--ha-space-4);

View File

@@ -57,7 +57,11 @@ import type { HomeAssistant } from "../../types";
import { isMac } from "../../util/is_mac";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
import type { QuickBarParams, QuickBarSection } from "./show-dialog-quick-bar";
import {
effectiveQuickBarMode,
type QuickBarParams,
type QuickBarSection,
} from "./show-dialog-quick-bar";
const SEPARATOR = "________";
@@ -100,7 +104,7 @@ export class QuickBar extends LitElement {
this._translationsLoaded = true;
}
this._initialize();
this._selectedSection = params.mode;
this._selectedSection = effectiveQuickBarMode(this.hass.user, params.mode);
this._showHint = params.showHint ?? false;
this._relatedResult = params.contextItem ? params.related : undefined;
@@ -656,8 +660,10 @@ export class QuickBar extends LitElement {
private _generateActionCommandsMemoized = memoizeOne(generateActionCommands);
private _createFuseIndex = (states, keys: FuseWeightedKey[]) =>
Fuse.createIndex(keys, states);
private _createFuseIndex = (
states: PickerComboBoxItem[],
keys: FuseWeightedKey[]
) => Fuse.createIndex(keys, states);
private _fuseIndexes = {
entity: memoizeOne((states: PickerComboBoxItem[]) =>

View File

@@ -1,5 +1,6 @@
import { fireEvent } from "../../common/dom/fire_event";
import type { ItemType, RelatedResult } from "../../data/search";
import type { HomeAssistant } from "../../types";
import { closeDialog } from "../make-dialog-manager";
export type QuickBarSection =
@@ -22,6 +23,20 @@ export interface QuickBarParams {
related?: RelatedResult;
}
/** Non-admin users cannot scope the bar to command, device, or area (those sections are admin-only). */
export const effectiveQuickBarMode = (
user: HomeAssistant["user"],
mode?: QuickBarSection
): QuickBarSection | undefined => {
if (mode && user?.is_admin) {
return mode;
}
if (mode === "command" || mode === "device" || mode === "area") {
return undefined;
}
return mode;
};
export const loadQuickBar = () => import("./ha-quick-bar");
export const showQuickBar = (

View File

@@ -1,4 +1,3 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import {
mdiAutoFix,
mdiLifebuoy,
@@ -19,6 +18,7 @@ import "../../components/ha-icon-next";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/ha-spinner";
import "../../components/progress/ha-progress-bar";
import { fetchBackupInfo } from "../../data/backup";
import type { BackupManagerState } from "../../data/backup_manager";
import {
@@ -120,9 +120,7 @@ class DialogRestart extends LitElement {
<div class="action-loader">
${this._loadingBackupInfo
? html`<ha-fade-in .delay=${250}>
<mwc-linear-progress
.indeterminate=${true}
></mwc-linear-progress>
<ha-progress-bar indeterminate></ha-progress-bar>
</ha-fade-in>`
: nothing}
</div>
@@ -464,7 +462,8 @@ class DialogRestart extends LitElement {
padding: 24px;
}
.action-loader {
height: 4px;
--ha-progress-bar-track-height: 4px;
--ha-progress-bar-border-radius: 0;
}
`,
];

View File

@@ -5,7 +5,7 @@ import type { LocalizeKeys } from "../../common/translations/localize";
import "../../components/ha-alert";
import "../../components/ha-dialog";
import "../../components/ha-svg-icon";
import { localizeContext } from "../../data/context";
import { internationalizationContext } from "../../data/context/context";
import { isMac } from "../../util/is_mac";
import { DialogMixin } from "../dialog-mixin";
@@ -168,8 +168,8 @@ const _SHORTCUTS: Section[] = [
@customElement("dialog-shortcuts")
class DialogShortcuts extends DialogMixin(LitElement) {
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
private _renderShortcut(
shortcutKeys: ShortcutString[],
@@ -183,13 +183,15 @@ class DialogShortcuts extends DialogMixin(LitElement) {
>${shortcutKey === CTRL_CMD
? isMac
? "⌘"
: this.localize("ui.dialogs.shortcuts.keys.ctrl")
: this._i18n.localize("ui.dialogs.shortcuts.keys.ctrl")
: typeof shortcutKey === "string"
? shortcutKey
: this.localize(shortcutKey.shortcutTranslationKey)}</span
: this._i18n.localize(
shortcutKey.shortcutTranslationKey
)}</span
>`
)}
${this.localize(descriptionKey)}
${this._i18n.localize(descriptionKey)}
</div>
`;
}
@@ -198,12 +200,12 @@ class DialogShortcuts extends DialogMixin(LitElement) {
return html`
<ha-dialog
open
.headerTitle=${this.localize("ui.dialogs.shortcuts.title")}
.headerTitle=${this._i18n.localize("ui.dialogs.shortcuts.title")}
>
<div class="content">
${_SHORTCUTS.map(
(section) => html`
<h3>${this.localize(section.titleTranslationKey)}</h3>
<h3>${this._i18n.localize(section.titleTranslationKey)}</h3>
<div class="items">
${section.items.map((item) => {
if ("shortcut" in item) {
@@ -213,7 +215,7 @@ class DialogShortcuts extends DialogMixin(LitElement) {
);
}
return html`<p>
${this.localize((item as Text).textTranslationKey)}
${this._i18n.localize((item as Text).textTranslationKey)}
</p>`;
})}
</div>
@@ -222,9 +224,9 @@ class DialogShortcuts extends DialogMixin(LitElement) {
</div>
<ha-alert slot="footer">
${this.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", {
${this._i18n.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", {
user_profile: html`<a href="/profile/general#shortcuts"
>${this.localize(
>${this._i18n.localize(
"ui.dialogs.shortcuts.enable_shortcuts_hint_user_profile"
)}</a
>`,

View File

@@ -1,4 +1,3 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { mdiDotsVertical, mdiRestart } from "@mdi/js";
import { css, html, LitElement, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";

View File

@@ -2,8 +2,8 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-progress-ring";
import "../../components/ha-spinner";
import "../../components/progress/ha-progress-ring";
import { ON, UNAVAILABLE } from "../../data/entity/entity";
import {
updateCanInstall,

View File

@@ -303,7 +303,6 @@ export const provideHass = (
debugConnection: false,
kioskMode: false,
suspendWhenHidden: false,
moreInfoEntityId: null as any,
// @ts-ignore
async callService(domain, service, data) {
if (data && "entity_id" in data) {

View File

@@ -199,6 +199,7 @@ class HassSubpage extends LitElement {
flex-wrap: wrap;
justify-content: flex-end;
gap: var(--ha-space-2);
--ha-button-box-shadow: var(--ha-box-shadow-l);
}
:host([narrow]) #fab.tabs {
bottom: calc(84px + var(--safe-area-inset-bottom, 0px));

View File

@@ -34,6 +34,8 @@ export interface PageNavigation {
not_component?: string | string[];
core?: boolean;
advancedOnly?: boolean;
/** Hide from non-admin users in filtered navigation and quick bar. */
adminOnly?: boolean;
iconPath?: string;
iconSecondaryPath?: string;
iconViewBox?: string;
@@ -420,6 +422,7 @@ export class HassTabsSubpage extends LitElement {
flex-wrap: wrap;
justify-content: flex-end;
gap: var(--ha-space-2);
--ha-button-box-shadow: var(--ha-box-shadow-l);
}
:host([narrow][show-tabs]) #fab {
bottom: calc(84px + var(--safe-area-inset-bottom, 0px));

View File

@@ -1,4 +1,3 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import type { Auth } from "home-assistant-js-websocket";
import {
createConnection,
@@ -16,6 +15,8 @@ import {
} from "../common/auth/token_storage";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import type { HASSDomEvent } from "../common/dom/fire_event";
import { mainWindow } from "../common/dom/get_main_window";
import { navigate } from "../common/navigate";
import {
addSearchParam,
extractSearchParam,
@@ -33,19 +34,18 @@ import {
onboardIntegrationStep,
} from "../data/onboarding";
import { subscribeUser } from "../data/ws-user";
import { makeDialogManager } from "../dialogs/make-dialog-manager";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { HassElement } from "../state/hass-element";
import type { HomeAssistant } from "../types";
import { storeState } from "../util/ha-pref-storage";
import { registerServiceWorker } from "../util/register-service-worker";
import "../components/progress/ha-progress-bar";
import "./onboarding-analytics";
import "./onboarding-create-user";
import "./onboarding-loading";
import "./onboarding-welcome";
import "./onboarding-welcome-links";
import { makeDialogManager } from "../dialogs/make-dialog-manager";
import { navigate } from "../common/navigate";
import { mainWindow } from "../common/dom/get_main_window";
type OnboardingEvent =
| {
@@ -126,9 +126,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
};
protected render() {
return html`<mwc-linear-progress
.progress=${this._progress}
></mwc-linear-progress>
return html`<ha-progress-bar .value=${this._progress}></ha-progress-bar>
<ha-card>
<div class="card-content">${this._renderStep()}</div>
</ha-card>
@@ -318,7 +316,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
history.replaceState(null, "", location.pathname);
await this._connectHass(auth);
const currentStep = steps.findIndex((stp) => !stp.done);
const singelStepProgress = 1 / steps.length;
const singelStepProgress = 100 / steps.length;
this._progress = currentStep * singelStepProgress + singelStepProgress;
} else {
this._init = true;
@@ -333,7 +331,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
}
private _handleProgress(ev: HASSDomEvent<OnboardingProgressEvent>) {
const stepSize = 1 / this._steps!.length;
const stepSize = 100 / this._steps!.length;
if (ev.detail.increase) {
this._progress += ev.detail.increase * stepSize;
}
@@ -355,7 +353,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
this._init = false;
this._restoring = stepResult.result?.restore;
if (!this._restoring) {
this._progress = 0.25;
this._progress = 25;
} else {
navigate(
`${location.pathname}?${addSearchParam({ page: `restore_backup${this._restoring === "cloud" ? "_cloud" : ""}` })}`
@@ -364,7 +362,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
} else if (stepResult.type === "user") {
const result = stepResult.result as OnboardingResponses["user"];
this._loading = true;
this._progress = 0.5;
this._progress = 50;
enableWrite();
try {
const auth = await getAuth({
@@ -381,10 +379,10 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
this._loading = false;
}
} else if (stepResult.type === "core_config") {
this._progress = 0.75;
this._progress = 75;
// We do nothing
} else if (stepResult.type === "analytics") {
this._progress = 1;
this._progress = 100;
// We do nothing
} else if (stepResult.type === "integration") {
this._loading = true;
@@ -505,7 +503,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
.card-content {
padding: 32px;
}
mwc-linear-progress {
ha-progress-bar {
--ha-progress-bar-border-radius: 0;
--ha-progress-bar-track-height: 4px;
position: fixed;
top: 0;
left: 0;

View File

@@ -1,12 +1,12 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../../components/ha-alert";
import "../../components/ha-button";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../../components/progress/ha-progress-bar";
import type { BackupOnboardingInfo } from "../../data/backup_onboarding";
import { onBoardingStyles } from "../styles";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("onboarding-restore-backup-status")
class OnboardingRestoreBackupStatus extends LitElement {
@@ -33,7 +33,7 @@ class OnboardingRestoreBackupStatus extends LitElement {
${this.backupInfo.state === "restore_backup"
? html`
<div class="loading">
<mwc-linear-progress indeterminate></mwc-linear-progress>
<ha-progress-bar indeterminate></ha-progress-bar>
</div>
`
: html`
@@ -92,7 +92,7 @@ class OnboardingRestoreBackupStatus extends LitElement {
padding: 16px 0;
font-size: var(--ha-font-size-l);
}
mwc-linear-progress {
ha-progress-bar {
width: 100%;
}
`,

View File

@@ -1,3 +1,4 @@
import { TZDate } from "@date-fns/tz";
import type { CalendarOptions } from "@fullcalendar/core";
import { Calendar } from "@fullcalendar/core";
import allLocales from "@fullcalendar/core/locales-all";
@@ -16,7 +17,6 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoize from "memoize-one";
import { TZDate } from "@date-fns/tz";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import { resolveTimeZone } from "../../common/datetime/resolve-time-zone";
import { useAmPm } from "../../common/datetime/use_am_pm";
@@ -25,7 +25,6 @@ import { supportsFeature } from "../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../../components/ha-button";
import "../../components/ha-button-toggle-group";
import "../../components/ha-fab";
import "../../components/ha-icon-button-next";
import "../../components/ha-icon-button-prev";
import type {
@@ -218,14 +217,10 @@ export class HAFullCalendar extends LitElement {
<div id="calendar"></div>
${this.addFab && this._hasMutableCalendars
? html`<ha-fab
slot="fab"
.label=${this.hass.localize("ui.components.calendar.event.add")}
extended
@click=${this._createEvent}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>`
? html`<ha-button size="large" slot="fab" @click=${this._createEvent}>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize("ui.components.calendar.event.add")}
</ha-button>`
: nothing}
`;
}
@@ -559,13 +554,14 @@ export class HAFullCalendar extends LitElement {
--ha-icon-button-size: 32px;
}
ha-fab {
ha-button[slot="fab"] {
position: absolute;
bottom: 16px;
right: 16px;
inset-inline-end: 16px;
bottom: var(--ha-space-4);
right: var(--ha-space-4);
inset-inline-end: var(--ha-space-4);
inset-inline-start: initial;
z-index: 1;
--ha-button-box-shadow: var(--ha-box-shadow-l);
}
#calendar {

View File

@@ -3,6 +3,7 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../common/translations/localize";
import type {
@@ -10,11 +11,10 @@ import type {
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-fab";
import "../../../components/ha-button";
import "../../../components/ha-help-tooltip";
import "../../../components/ha-svg-icon";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-svg-icon";
import type { ApplicationCredential } from "../../../data/application_credential";
import {
deleteApplicationCredential,
@@ -30,7 +30,6 @@ import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-
import type { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showAddApplicationCredentialDialog } from "./show-dialog-add-application-credential";
import { storage } from "../../../common/decorators/storage";
@customElement("ha-config-application-credentials")
export class HaConfigApplicationCredentials extends LitElement {
@@ -201,16 +200,16 @@ export class HaConfigApplicationCredentials extends LitElement {
</ha-help-tooltip>
`}
</div>
<ha-fab
<ha-button
slot="fab"
.label=${this.hass.localize(
"ui.panel.config.application_credentials.picker.add_application_credential"
)}
extended
size="large"
@click=${this._addApplicationCredential}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.application_credentials.picker.add_application_credential"
)}
</ha-button>
</hass-tabs-subpage-data-table>
`;
}

View File

@@ -0,0 +1,179 @@
import { mdiOpenInNew, mdiPuzzle } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-svg-icon";
import { saveFrontendUserData } from "../../../data/frontend";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { navigate } from "../../../common/navigate";
@customElement("ha-config-apps-info")
class HaConfigAppsInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
protected render(): TemplateResult {
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.header=${this.hass.localize("ui.panel.config.dashboard.apps.main")}
>
<div class="content">
<ha-card outlined>
<div class="card-content">
<div class="header">
<ha-svg-icon class="icon" .path=${mdiPuzzle}></ha-svg-icon>
<h1>
${this.hass.localize(
"ui.panel.config.apps.info.what_is_an_app"
)}
</h1>
</div>
<p>
${this.hass.localize(
"ui.panel.config.apps.info.what_is_an_app_description"
)}
</p>
</div>
</ha-card>
<ha-card outlined>
<div class="card-content">
<h2>
${this.hass.localize(
"ui.panel.config.apps.info.why_not_available"
)}
</h2>
<p>
${this.hass.localize(
"ui.panel.config.apps.info.why_not_available_description"
)}
</p>
<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.apps.info.installation_hint"
)}
</ha-alert>
</div>
<div class="card-actions">
<ha-button
appearance="plain"
href=${documentationUrl(this.hass, "/apps/")}
target="_blank"
rel="noreferrer"
>
${this.hass.localize("ui.panel.config.apps.info.learn_more")}
<ha-svg-icon slot="icon" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-button>
<ha-button @click=${this._dismiss} variant="danger">
${this.hass.localize("ui.panel.config.apps.info.dismiss")}
</ha-button>
</div>
</ha-card>
</div>
</hass-subpage>
`;
}
private async _dismiss(): Promise<void> {
try {
await saveFrontendUserData(this.hass.connection, "core", {
...this.hass.userData,
apps_info_dismissed: true,
});
} catch (err) {
showAlertDialog(this, { text: (err as Error).message });
return;
}
navigate("/config", { replace: true });
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.content {
max-width: 600px;
margin: 0 auto;
padding: var(--ha-space-4);
display: flex;
flex-direction: column;
gap: var(--ha-space-4);
}
.card-content {
padding: var(--ha-space-4);
}
.header {
display: flex;
align-items: center;
gap: var(--ha-space-3);
margin-bottom: var(--ha-space-4);
}
.icon {
width: 40px;
height: 40px;
flex-shrink: 0;
color: var(--primary-color);
}
h1 {
margin: 0;
font-size: var(--ha-font-size-xl);
font-weight: 500;
}
h2 {
margin: 0 0 var(--ha-space-3);
font-size: var(--ha-font-size-l);
font-weight: 500;
}
p {
margin: 0 0 var(--ha-space-3);
line-height: var(--ha-line-height-normal);
color: var(--secondary-text-color);
}
ha-alert {
display: block;
margin-top: var(--ha-space-2);
}
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--ha-space-2);
padding: var(--ha-space-2);
border-top: var(--ha-border-width-sm) solid var(--divider-color);
}
a {
text-decoration: none;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-apps-info": HaConfigAppsInfo;
}
}

View File

@@ -10,8 +10,8 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "../../../components/input/ha-input-search";
@@ -157,16 +157,10 @@ export class HaConfigAppsInstalled extends LitElement {
</div>
</div>
<a href="/config/apps/available">
<ha-fab
.label=${this.hass.localize(
"ui.panel.config.apps.installed.add_app"
)}
extended
>
<ha-svg-icon slot="icon" .path=${mdiStorePlus}></ha-svg-icon>
</ha-fab>
</a>
<ha-button size="large" href="/config/apps/available">
<ha-svg-icon slot="start" .path=${mdiStorePlus}></ha-svg-icon>
${this.hass.localize("ui.panel.config.apps.installed.add_app")}
</ha-button>
</hass-subpage>
`;
}
@@ -270,7 +264,7 @@ export class HaConfigAppsInstalled extends LitElement {
cursor: pointer;
}
ha-fab {
ha-button[size="large"] {
position: fixed;
right: calc(var(--ha-space-4) + var(--safe-area-inset-right));
bottom: calc(var(--ha-space-4) + var(--safe-area-inset-bottom));
@@ -279,6 +273,7 @@ export class HaConfigAppsInstalled extends LitElement {
);
inset-inline-start: initial;
z-index: 1;
--ha-button-box-shadow: var(--ha-box-shadow-l);
}
`,
];

View File

@@ -5,7 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import "../../../components/data-table/ha-data-table";
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import "../../../components/ha-fab";
import "../../../components/ha-button";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../data/hassio/common";
@@ -116,13 +116,10 @@ export class HaConfigAppsRegistries extends LitElement {
id="registry"
has-fab
></ha-data-table>
<ha-fab
.label=${this.hass.localize("ui.panel.config.apps.registries.add")}
extended
@click=${this._showAddRegistryDialog}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
<ha-button size="large" @click=${this._showAddRegistryDialog}>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize("ui.panel.config.apps.registries.add")}
</ha-button>
</hass-subpage>
`;
}
@@ -187,13 +184,14 @@ export class HaConfigAppsRegistries extends LitElement {
ha-icon-button.delete {
color: var(--error-color);
}
ha-fab {
ha-button[size="large"] {
position: fixed;
right: calc(var(--ha-space-4) + var(--safe-area-inset-right));
bottom: calc(var(--ha-space-4) + var(--safe-area-inset-bottom));
inset-inline-end: calc(var(--ha-space-4) + var(--safe-area-inset-right));
inset-inline-start: initial;
z-index: 1;
--ha-button-box-shadow: var(--ha-box-shadow-l);
}
`;
}

View File

@@ -8,14 +8,14 @@ import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/data-table/ha-data-table";
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import "../../../components/ha-fab";
import "../../../components/ha-button";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import type {
HassioAddonInfo,
HassioAddonsInfo,
HassioAddonRepository,
HassioAddonsInfo,
} from "../../../data/hassio/addon";
import { fetchHassioAddonsInfo } from "../../../data/hassio/addon";
import { extractApiErrorMessage } from "../../../data/hassio/common";
@@ -195,13 +195,10 @@ export class HaConfigAppsRepositories extends LitElement {
id="slug"
has-fab
></ha-data-table>
<ha-fab
.label=${this.hass.localize("ui.panel.config.apps.repositories.add")}
extended
@click=${this._showAddRepositoryDialog}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
<ha-button size="large" @click=${this._showAddRepositoryDialog}>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize("ui.panel.config.apps.repositories.add")}
</ha-button>
</hass-subpage>
`;
}
@@ -295,13 +292,14 @@ export class HaConfigAppsRepositories extends LitElement {
ha-icon-button.delete {
color: var(--error-color);
}
ha-fab {
ha-button[size="large"] {
position: fixed;
right: calc(var(--ha-space-4) + var(--safe-area-inset-right));
bottom: calc(var(--ha-space-4) + var(--safe-area-inset-bottom));
inset-inline-end: calc(var(--ha-space-4) + var(--safe-area-inset-right));
inset-inline-start: initial;
z-index: 1;
--ha-button-box-shadow: var(--ha-box-shadow-l);
}
`;
}

View File

@@ -1,4 +1,5 @@
import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import type { RouterOptions } from "../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../layouts/hass-router-page";
import type { HomeAssistant, Route } from "../../../types";
@@ -15,6 +16,12 @@ class HaConfigApps extends HassRouterPage {
protected routerOptions: RouterOptions = {
defaultPage: "installed",
beforeRender: () => {
if (!isComponentLoaded(this.hass.config, "hassio")) {
return "info";
}
return undefined;
},
routes: {
installed: {
tag: "ha-config-apps-installed",
@@ -32,6 +39,10 @@ class HaConfigApps extends HassRouterPage {
tag: "ha-config-apps-registries",
load: () => import("./ha-config-apps-registries"),
},
info: {
tag: "ha-config-apps-info",
load: () => import("./ha-config-apps-info"),
},
},
};

View File

@@ -30,7 +30,7 @@ import {
updateAreaRegistryEntry,
} from "../../../data/area/area_registry";
import type { AutomationEntity } from "../../../data/automation";
import { fullEntitiesContext } from "../../../data/context";
import { fullEntitiesContext } from "../../../data/context/context";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import { sortDeviceRegistryByName } from "../../../data/device/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";

View File

@@ -1,4 +1,6 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import "@home-assistant/webawesome/dist/components/popover/popover";
import type WaPopover from "@home-assistant/webawesome/dist/components/popover/popover";
import {
mdiDelete,
mdiDotsVertical,
@@ -15,7 +17,7 @@ import {
type PropertyValues,
type TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import {
@@ -24,9 +26,10 @@ import {
type AreasFloorHierarchy,
} from "../../../common/areas/areas-floor-hierarchy";
import { formatListWithAnds } from "../../../common/string/format-list";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-floor-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-sortable";
@@ -58,7 +61,6 @@ import {
} from "./show-dialog-area-registry-detail";
import { showAreasFloorsOrderDialog } from "./show-dialog-areas-floors-order";
import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-detail";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
const UNASSIGNED_FLOOR = "__unassigned__";
@@ -84,10 +86,12 @@ export class HaConfigAreasDashboard extends LitElement {
@property({ attribute: false }) public route!: Route;
private _searchParms = new URLSearchParams(window.location.search);
@state() private _hierarchy?: AreasFloorHierarchy;
@query("wa-popover") private _popover?: WaPopover;
private _searchParms = new URLSearchParams(window.location.search);
private _blockHierarchyUpdate = false;
private _blockHierarchyUpdateTimeout?: number;
@@ -318,27 +322,26 @@ export class HaConfigAreasDashboard extends LitElement {
`
: nothing}
</div>
<ha-fab
slot="fab"
class="floor"
.label=${this.hass.localize(
"ui.panel.config.areas.picker.create_floor"
)}
extended
@click=${this._createFloor}
<ha-button id="fab" slot="fab" size="large">
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize("ui.common.add")}
</ha-button>
<wa-popover
trap-focus
placement="top-start"
distance="8"
without-arrow
for="fab"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
<ha-fab
slot="fab"
.label=${this.hass.localize(
"ui.panel.config.areas.picker.create_area"
)}
extended
@click=${this._createArea}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
<ha-button appearance="filled" @click=${this._createFloor}>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize("ui.panel.config.areas.picker.create_floor")}
</ha-button>
<ha-button appearance="filled" @click=${this._createArea}>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize("ui.panel.config.areas.picker.create_area")}
</ha-button>
</wa-popover>
</hass-tabs-subpage>
`;
}
@@ -559,6 +562,7 @@ export class HaConfigAreasDashboard extends LitElement {
}
private _createFloor() {
this._popover?.hide();
this._openFloorDialog();
}
@@ -584,6 +588,7 @@ export class HaConfigAreasDashboard extends LitElement {
}
private _createArea() {
this._popover?.hide();
this._openAreaDialog();
}
@@ -725,6 +730,14 @@ export class HaConfigAreasDashboard extends LitElement {
align-items: center;
overflow-wrap: anywhere;
}
wa-popover::part(body) {
gap: var(--ha-space-2);
background-color: transparent;
border-color: transparent;
box-shadow: none;
padding: 0;
}
`;
}

View File

@@ -31,6 +31,8 @@ import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { computeObjectId } from "../../../../common/entity/compute_object_id";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
@@ -57,7 +59,7 @@ import type {
} from "../../../../data/automation";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import { fullEntitiesContext } from "../../../../data/context/context";
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type {
Action,
@@ -95,8 +97,6 @@ import "./types/ha-automation-action-set_conversation_response";
import "./types/ha-automation-action-stop";
import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { computeObjectId } from "../../../../common/entity/compute_object_id";
export const getAutomationActionType = memoizeOne(
(action: Action | undefined) => {

View File

@@ -7,7 +7,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/device/ha-device-action-picker";
import "../../../../../components/device/ha-device-picker";
import "../../../../../components/ha-form/ha-form";
import { fullEntitiesContext } from "../../../../../data/context";
import { fullEntitiesContext } from "../../../../../data/context/context";
import type {
DeviceAction,
DeviceCapabilities,

View File

@@ -6,7 +6,7 @@ import "../../../../../components/ha-formfield";
import "../../../../../components/ha-switch";
import "../../../../../components/input/ha-input";
import type { HaInput } from "../../../../../components/input/ha-input";
import { localizeContext } from "../../../../../data/context";
import { internationalizationContext } from "../../../../../data/context/context";
import type { StopAction } from "../../../../../data/script";
import type { ActionElement } from "../ha-automation-action-row";
@@ -17,8 +17,8 @@ export class HaStopAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public disabled = false;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
public static get defaultConfig(): StopAction {
return { stop: "" };
@@ -29,7 +29,7 @@ export class HaStopAction extends LitElement implements ActionElement {
return html`
<ha-input
.label=${this.localize(
.label=${this._i18n.localize(
"ui.panel.config.automation.editor.actions.type.stop.stop"
)}
.value=${stop}
@@ -37,7 +37,7 @@ export class HaStopAction extends LitElement implements ActionElement {
@change=${this._stopChanged}
></ha-input>
<ha-input
.label=${this.localize(
.label=${this._i18n.localize(
"ui.panel.config.automation.editor.actions.type.stop.response_variable"
)}
.value=${response_variable || ""}
@@ -46,7 +46,7 @@ export class HaStopAction extends LitElement implements ActionElement {
></ha-input>
<ha-formfield
.disabled=${this.disabled}
.label=${this.localize(
.label=${this._i18n.localize(
"ui.panel.config.automation.editor.actions.type.stop.error"
)}
>

View File

@@ -85,7 +85,7 @@ import {
getConfigEntries,
type ConfigEntry,
} from "../../../data/config_entries";
import { labelsContext } from "../../../data/context";
import { labelsContext } from "../../../data/context/context";
import { getDeviceEntityLookup } from "../../../data/device/device_registry";
import type { EntityComboBoxItem } from "../../../data/entity/entity_picker";
import { getFloorAreaLookup } from "../../../data/floor_registry";
@@ -99,9 +99,9 @@ import {
domainToName,
fetchIntegrationManifests,
} from "../../../data/integration";
import { filterSelectorEntities } from "../../../data/selector";
import type { LabelRegistryEntry } from "../../../data/label/label_registry";
import { subscribeLabFeature } from "../../../data/labs";
import { filterSelectorEntities } from "../../../data/selector";
import {
TARGET_SEPARATOR,
getConditionsForTarget,

View File

@@ -1,7 +1,7 @@
import "@home-assistant/webawesome/dist/components/tree-item/tree-item";
import "@home-assistant/webawesome/dist/components/tree/tree";
import type { WaSelectionChangeEvent } from "@home-assistant/webawesome/dist/events/selection-change";
import { consume } from "@lit/context";
import { consume, type ContextType } from "@lit/context";
import { mdiTextureBox } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import {
@@ -44,14 +44,11 @@ import {
type ConfigEntry,
} from "../../../../data/config_entries";
import {
areasContext,
devicesContext,
entitiesContext,
floorsContext,
internationalizationContext,
labelsContext,
localizeContext,
registriesContext,
statesContext,
} from "../../../../data/context";
} from "../../../../data/context/context";
import { getDeviceEntityLookup } from "../../../../data/device/device_registry";
import {
domainToName,
@@ -99,28 +96,14 @@ export default class HaAutomationAddFromTarget extends LitElement {
// #region context
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: HomeAssistant["localize"];
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: statesContext, subscribe: true })
private states!: HomeAssistant["states"];
private states!: ContextType<typeof statesContext>;
@state()
@consume({ context: floorsContext, subscribe: true })
private floors!: HomeAssistant["floors"];
@state()
@consume({ context: areasContext, subscribe: true })
private areas!: HomeAssistant["areas"];
@state()
@consume({ context: devicesContext, subscribe: true })
private devices!: HomeAssistant["devices"];
@state()
@consume({ context: entitiesContext, subscribe: true })
private entities!: HomeAssistant["entities"];
@consume({ context: registriesContext, subscribe: true })
private _registries!: ContextType<typeof registriesContext>;
@state()
@consume({ context: labelsContext, subscribe: true })
@@ -205,7 +188,9 @@ export default class HaAutomationAddFromTarget extends LitElement {
? html`
<div class="targets-show-more">
<ha-button appearance="filled" @click=${this._expandHeight}>
${this.localize("ui.panel.config.automation.editor.show_more")}
${this._i18n.localize(
"ui.panel.config.automation.editor.show_more"
)}
</ha-button>
</div>
`
@@ -235,7 +220,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
if (valueType === "area" && valueId) {
const floor =
entries[
`floor${TARGET_SEPARATOR}${this.areas[valueId]?.floor_id || ""}`
`floor${TARGET_SEPARATOR}${this._registries.areas[valueId]?.floor_id || ""}`
];
const { devices, entities } =
floor.areas![`area${TARGET_SEPARATOR}${valueId}`];
@@ -255,9 +240,9 @@ export default class HaAutomationAddFromTarget extends LitElement {
}
if (valueId && valueType === "device") {
const areaId = this.devices[valueId]?.area_id;
const areaId = this._registries.devices[valueId]?.area_id;
if (areaId) {
const floorId = this.areas[areaId]?.floor_id || "";
const floorId = this._registries.areas[areaId]?.floor_id || "";
const { entities } =
entries[`floor${TARGET_SEPARATOR}${floorId}`].areas![
`area${TARGET_SEPARATOR}${areaId}`
@@ -266,7 +251,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
return entities.length ? this._renderEntities(entities) : nothing;
}
const device = this.devices[valueId];
const device = this._registries.devices[valueId];
const isService = device.entry_type === "service";
const { entities } =
entries[`${isService ? "service" : "area"}${TARGET_SEPARATOR}`]
@@ -316,7 +301,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
)
: this._renderItem(
!floor.id
? this.localize(
? this._i18n.localize(
"ui.panel.config.automation.editor.other_areas"
)
: floor.primary,
@@ -337,7 +322,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
)
);
return html`<ha-section-title
>${this.localize(
>${this._i18n.localize(
"ui.panel.config.automation.editor.home"
)}</ha-section-title
>
@@ -345,7 +330,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
? html`<ha-md-list>
<ha-md-list-item type="text">
<div slot="headline">
${this.localize("ui.components.area-picker.no_areas")}
${this._i18n.localize("ui.components.area-picker.no_areas")}
</div>
</ha-md-list-item>
</ha-md-list>`
@@ -362,9 +347,9 @@ export default class HaAutomationAddFromTarget extends LitElement {
(narrow: boolean, value?: SingleHassServiceTarget) => {
const labels = this._getLabelsMemoized(
this.states,
this.areas,
this.devices,
this.entities,
this._registries.areas,
this._registries.devices,
this._registries.entities,
this._labelRegistry,
undefined,
undefined,
@@ -380,7 +365,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
}
return html`<ha-section-title
>${this.localize(
>${this._i18n.localize(
"ui.components.label-picker.labels"
)}</ha-section-title
>
@@ -447,7 +432,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
const open = entries[`device${TARGET_SEPARATOR}`].open;
items.push(
this._renderItem(
this.localize("ui.components.target-picker.type.entities"),
this._i18n.localize("ui.components.target-picker.type.entities"),
`device${TARGET_SEPARATOR}`,
true,
false,
@@ -468,7 +453,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
const open = entries[`helper${TARGET_SEPARATOR}`].open;
items.push(
this._renderItem(
this.localize("ui.panel.config.automation.editor.helpers"),
this._i18n.localize("ui.panel.config.automation.editor.helpers"),
`helper${TARGET_SEPARATOR}`,
true,
false,
@@ -489,7 +474,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
const open = entries[`area${TARGET_SEPARATOR}`].open;
items.push(
this._renderItem(
this.localize("ui.components.target-picker.type.devices"),
this._i18n.localize("ui.components.target-picker.type.devices"),
`area${TARGET_SEPARATOR}`,
true,
false,
@@ -507,7 +492,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
const open = entries[`service${TARGET_SEPARATOR}`].open;
items.push(
this._renderItem(
this.localize("ui.panel.config.automation.editor.services"),
this._i18n.localize("ui.panel.config.automation.editor.services"),
`service${TARGET_SEPARATOR}`,
true,
false,
@@ -524,7 +509,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
}
return html`<ha-section-title
>${this.localize(
>${this._i18n.localize(
"ui.panel.config.automation.editor.unassigned"
)}</ha-section-title
>${narrow
@@ -539,11 +524,11 @@ export default class HaAutomationAddFromTarget extends LitElement {
const renderedAreas = Object.keys(areas)
.filter((areaTargetId) => {
const [, areaId] = areaTargetId.split(TARGET_SEPARATOR, 2);
return this.areas[areaId];
return this._registries.areas[areaId];
})
.map((areaTargetId) => {
const [, areaId] = areaTargetId.split(TARGET_SEPARATOR, 2);
const area = this.areas[areaId];
const area = this._registries.areas[areaId];
return [
areaTargetId,
computeAreaName(area) || area.area_id,
@@ -578,7 +563,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
if (this.narrow) {
return html`<ha-section-title
>${this.localize(
>${this._i18n.localize(
"ui.components.target-picker.type.areas"
)}</ha-section-title
>
@@ -590,9 +575,9 @@ export default class HaAutomationAddFromTarget extends LitElement {
private _renderDevices(devices: Record<string, Level3Entries>) {
const renderedDevices = Object.keys(devices)
.filter((deviceId) => this.devices[deviceId])
.filter((deviceId) => this._registries.devices[deviceId])
.map((deviceId) => {
const device = this.devices[deviceId];
const device = this._registries.devices[deviceId];
const configEntry = device.primary_config_entry
? this._configEntryLookup?.[device.primary_config_entry]
: undefined;
@@ -627,7 +612,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
if (this.narrow) {
return html`<ha-section-title
>${this.localize(
>${this._i18n.localize(
"ui.components.target-picker.type.devices"
)}</ha-section-title
>
@@ -648,7 +633,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
domainTargetId.length - TARGET_SEPARATOR.length
);
const label = domainToName(
this.localize,
this._i18n.localize,
domain,
this.manifests![domain]
);
@@ -674,7 +659,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
if (this.narrow) {
return html`<ha-section-title
>${this.localize(
>${this._i18n.localize(
"ui.components.target-picker.type.devices"
)}</ha-section-title
>
@@ -697,16 +682,16 @@ export default class HaAutomationAddFromTarget extends LitElement {
const [entityName, deviceName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.entities,
this.devices,
this.areas,
this.floors
this._registries.entities,
this._registries.devices,
this._registries.areas,
this._registries.floors
);
let label = entityName || deviceName || entityId;
if (this.entities[entityId]?.hidden) {
label += ` (${this.localize("ui.panel.config.automation.editor.entity_hidden")})`;
if (this._registries.entities[entityId]?.hidden) {
label += ` (${this._i18n.localize("ui.panel.config.automation.editor.entity_hidden")})`;
}
return [entityId, label, stateObj] as [string, string, HassEntity];
@@ -729,7 +714,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
if (this.narrow) {
return html`<ha-section-title
>${this.localize(
>${this._i18n.localize(
"ui.components.target-picker.type.entities"
)}</ha-section-title
>
@@ -867,10 +852,10 @@ export default class HaAutomationAddFromTarget extends LitElement {
private _getTreeData() {
this._floorAreas = getAreasNestedInFloors(
this.states,
this.floors,
this.areas,
this.devices,
this.entities,
this._registries.floors,
this._registries.areas,
this._registries.devices,
this._registries.entities,
this._formatId,
undefined,
undefined,
@@ -903,7 +888,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
}
private _loadUnassignedDevices() {
const unassignedDevices = Object.values(this.devices).filter(
const unassignedDevices = Object.values(this._registries.devices).filter(
(device) => !device.area_id
);
@@ -912,16 +897,16 @@ export default class HaAutomationAddFromTarget extends LitElement {
const services: Record<string, Level3Entries> = {};
unassignedDevices.forEach(({ id: deviceId, entry_type }) => {
const device = this.devices[deviceId];
const device = this._registries.devices[deviceId];
if (!device || device.disabled_by) {
return;
}
const deviceEntry = {
open: false,
entities:
this._getDeviceEntityLookupMemoized(this.entities)[deviceId]?.map(
(entity) => entity.entity_id
) || [],
this._getDeviceEntityLookupMemoized(this._registries.entities)[
deviceId
]?.map((entity) => entity.entity_id) || [],
};
if (entry_type === "service") {
services[deviceId] = deviceEntry;
@@ -953,7 +938,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
}
private _loadUnassignedEntities() {
Object.values(this.entities)
Object.values(this._registries.entities)
.filter((entity) => !entity.area_id && !entity.device_id)
.forEach(({ entity_id }) => {
const domain = entity_id.split(".", 2)[0];
@@ -1010,23 +995,23 @@ export default class HaAutomationAddFromTarget extends LitElement {
private _loadArea(area: FloorComboBoxItem) {
const [, id] = area.id.split(TARGET_SEPARATOR, 2);
const referenced_devices =
this._getAreaDeviceLookupMemoized(this.devices)[id] || [];
this._getAreaDeviceLookupMemoized(this._registries.devices)[id] || [];
const referenced_entities =
this._getAreaEntityLookupMemoized(this.entities)[id] || [];
this._getAreaEntityLookupMemoized(this._registries.entities)[id] || [];
const devices: Record<string, Level3Entries> = {};
referenced_devices.forEach(({ id: deviceId }) => {
const device = this.devices[deviceId];
const device = this._registries.devices[deviceId];
if (!device || device.disabled_by) {
return;
}
devices[deviceId] = {
open: false,
entities:
this._getDeviceEntityLookupMemoized(this.entities)[deviceId]?.map(
(entity) => entity.entity_id
) || [],
this._getDeviceEntityLookupMemoized(this._registries.entities)[
deviceId
]?.map((entity) => entity.entity_id) || [],
};
});
@@ -1051,17 +1036,17 @@ export default class HaAutomationAddFromTarget extends LitElement {
}
if (type === "entity") {
const deviceId = this.entities[id]?.device_id;
const device = deviceId ? this.devices[deviceId] : undefined;
const deviceId = this._registries.entities[id]?.device_id;
const device = deviceId ? this._registries.devices[deviceId] : undefined;
const deviceAreaId = (deviceId && device?.area_id) || undefined;
if (!deviceAreaId) {
let floor: string;
let area: string;
const entity = this.entities[id];
const entity = this._registries.entities[id];
if (!deviceId && entity.area_id) {
floor = `floor${TARGET_SEPARATOR}${this.areas[entity.area_id]?.floor_id || ""}`;
floor = `floor${TARGET_SEPARATOR}${this._registries.areas[entity.area_id]?.floor_id || ""}`;
area = `area${TARGET_SEPARATOR}${entity.area_id}`;
} else if (!deviceId) {
const domain = id.split(".", 1)[0];
@@ -1093,7 +1078,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
return;
}
const floor = `floor${TARGET_SEPARATOR}${this.areas[deviceAreaId]?.floor_id || ""}`;
const floor = `floor${TARGET_SEPARATOR}${this._registries.areas[deviceAreaId]?.floor_id || ""}`;
const area = `area${TARGET_SEPARATOR}${deviceAreaId}`;
this._entries = {
@@ -1121,10 +1106,10 @@ export default class HaAutomationAddFromTarget extends LitElement {
}
if (type === "device") {
const deviceAreaId = this.devices[id]?.area_id;
const deviceAreaId = this._registries.devices[id]?.area_id;
if (!deviceAreaId) {
const device = this.devices[id];
const device = this._registries.devices[id];
const floor = `${device.entry_type === "service" ? "service" : "area"}${TARGET_SEPARATOR}`;
this._entries = {
...this._entries,
@@ -1136,7 +1121,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
return;
}
const floor = `floor${TARGET_SEPARATOR}${this.areas[deviceAreaId]?.floor_id || ""}`;
const floor = `floor${TARGET_SEPARATOR}${this._registries.areas[deviceAreaId]?.floor_id || ""}`;
const area = `area${TARGET_SEPARATOR}${deviceAreaId}`;
this._entries = {
@@ -1157,7 +1142,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
}
if (type === "area") {
const floor = `floor${TARGET_SEPARATOR}${this.areas[id]?.floor_id || ""}`;
const floor = `floor${TARGET_SEPARATOR}${this._registries.areas[id]?.floor_id || ""}`;
this._entries = {
...this._entries,
[floor]: {
@@ -1231,7 +1216,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
}
if (type === "area" && id) {
const floorId = `floor${TARGET_SEPARATOR}${this.areas[id]?.floor_id || ""}`;
const floorId = `floor${TARGET_SEPARATOR}${this._registries.areas[id]?.floor_id || ""}`;
this._entries = {
...this._entries,
@@ -1272,10 +1257,10 @@ export default class HaAutomationAddFromTarget extends LitElement {
}
if (type === "device" && id) {
const areaId = this.devices[id]?.area_id;
const areaId = this._registries.devices[id]?.area_id;
if (areaId) {
const areaTargetId = `area${TARGET_SEPARATOR}${this.devices[id]?.area_id ?? ""}`;
const floorId = `floor${TARGET_SEPARATOR}${(areaId && this.areas[areaId]?.floor_id) || ""}`;
const areaTargetId = `area${TARGET_SEPARATOR}${this._registries.devices[id]?.area_id ?? ""}`;
const floorId = `floor${TARGET_SEPARATOR}${(areaId && this._registries.areas[areaId]?.floor_id) || ""}`;
this._entries = {
...this._entries,
@@ -1300,7 +1285,9 @@ export default class HaAutomationAddFromTarget extends LitElement {
}
const deviceType =
this.devices[id]?.entry_type === "service" ? "service" : "area";
this._registries.devices[id]?.entry_type === "service"
? "service"
: "area";
const floorId = `${deviceType}${TARGET_SEPARATOR}`;
this._entries = {
...this._entries,

View File

@@ -40,7 +40,7 @@ import {
} from "../../../../data/area_floor_picker";
import { CONDITION_BUILDING_BLOCKS_GROUP } from "../../../../data/condition";
import type { ConfigEntry } from "../../../../data/config_entries";
import { labelsContext } from "../../../../data/context";
import { labelsContext } from "../../../../data/context/context";
import {
deviceComboBoxKeys,
getDevices,
@@ -450,8 +450,10 @@ export class HaAutomationAddSearch extends LitElement {
private _keyFunction = (item: PickerComboBoxItem | string) =>
typeof item === "string" ? item : item.id;
private _createFuseIndex = (states, keys: FuseWeightedKey[]) =>
Fuse.createIndex(keys, states);
private _createFuseIndex = (
states: PickerComboBoxItem[],
keys: FuseWeightedKey[]
) => Fuse.createIndex(keys, states);
private _fuseIndexes = {
area: memoizeOne((states: PickerComboBoxItem[]) =>

View File

@@ -6,7 +6,6 @@ import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-markdown";
import "../../../components/ha-fab";
import type { BlueprintAutomationConfig } from "../../../data/automation";
import { fetchBlueprints } from "../../../data/blueprint";
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
@@ -56,16 +55,16 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
: nothing}
${this.renderCard()}
<ha-fab
<ha-button
slot="fab"
size="large"
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this.saving}
extended
@click=${this._saveAutomation}
>
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab>
<ha-svg-icon slot="start" .path=${mdiContentSave}></ha-svg-icon>
${this.hass.localize("ui.common.save")}
</ha-button>
`;
}
@@ -109,8 +108,9 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
)
);
}
ha-fab {
ha-button[slot="fab"] {
position: fixed;
--ha-button-box-shadow: var(--ha-box-shadow-l);
}
`,
];

View File

@@ -36,6 +36,7 @@ import type { HaAutomationRow } from "../../../../components/ha-automation-row";
import "../../../../components/ha-card";
import "../../../../components/ha-condition-icon";
import "../../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
@@ -50,7 +51,7 @@ import { describeCondition } from "../../../../data/automation_i18n";
import type { ConditionDescriptions } from "../../../../data/condition";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import { fullEntitiesContext } from "../../../../data/context/context";
import type { DeviceCondition } from "../../../../data/device/device_automation";
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
import {
@@ -76,7 +77,6 @@ import "./types/ha-automation-condition-template";
import "./types/ha-automation-condition-time";
import "./types/ha-automation-condition-trigger";
import "./types/ha-automation-condition-zone";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
export interface ConditionElement extends LitElement {
condition: Condition;

View File

@@ -7,7 +7,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/device/ha-device-condition-picker";
import "../../../../../components/device/ha-device-picker";
import "../../../../../components/ha-form/ha-form";
import { fullEntitiesContext } from "../../../../../data/context";
import { fullEntitiesContext } from "../../../../../data/context/context";
import type {
DeviceCapabilities,
DeviceCondition,

View File

@@ -32,7 +32,6 @@ import { promiseTimeout } from "../../../common/util/promise-timeout";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
@@ -148,7 +147,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
this._newAutomationId &&
changedProps.has("entityRegistry")
) {
const automation = this.entityRegistry.find(
const automation = this.entityRegistry?.find(
(entity: EntityRegistryEntry) =>
entity.platform === "automation" &&
entity.unique_id === this._newAutomationId
@@ -547,19 +546,19 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
.showErrors=${false}
disable-fullscreen
></ha-yaml-editor>
<ha-fab
<ha-button
slot="fab"
size="large"
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this.saving}
extended
@click=${this._handleSaveAutomation}
>
<ha-svg-icon
slot="icon"
slot="start"
.path=${mdiContentSave}
></ha-svg-icon>
</ha-fab>`
${this.hass.localize("ui.common.save")}
</ha-button>`
: nothing}
</div>
</hass-subpage>

View File

@@ -45,13 +45,13 @@ import type {
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import type {
HaDropdown,
HaDropdownSelectEvent,
} from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-filter-blueprints";
import "../../../components/ha-filter-categories";
import "../../../components/ha-filter-devices";
@@ -79,7 +79,10 @@ import {
subscribeCategoryRegistry,
} from "../../../data/category_registry";
import type { CloudStatus } from "../../../data/cloud";
import { fullEntitiesContext, labelsContext } from "../../../data/context";
import {
fullEntitiesContext,
labelsContext,
} from "../../../data/context/context";
import type { DataTableFilters } from "../../../data/data_table_filters";
import {
deserializeFilters,
@@ -691,16 +694,12 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
</ha-button>
</div>`
: nothing}
<ha-fab
slot="fab"
.label=${this.hass.localize(
<ha-button slot="fab" size="large" @click=${this._createNew}>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.picker.add_automation"
)}
extended
@click=${this._createNew}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</ha-button>
</hass-tabs-subpage-data-table>
<ha-dropdown
id="overflow-menu"

View File

@@ -7,7 +7,7 @@ import { goBack, navigate } from "../../../common/navigate";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-fade-in";
import "../../../components/ha-spinner"; // used by renderLoading() provided to both editors
import { fullEntitiesContext } from "../../../data/context";
import { fullEntitiesContext } from "../../../data/context/context";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import {
showAlertDialog,
@@ -50,13 +50,14 @@ export const automationScriptEditorStyles: CSSResult = css`
p {
margin-bottom: 0;
}
ha-fab {
ha-button[slot="fab"] {
position: fixed;
right: calc(16px + var(--safe-area-inset-right, 0px));
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
--ha-button-box-shadow: var(--ha-box-shadow-l);
}
ha-fab.dirty {
ha-button[slot="fab"].dirty {
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
}
ha-tooltip ha-svg-icon {
@@ -93,7 +94,7 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
entityRegistry!: EntityRegistryEntry[];
entityRegistry?: EntityRegistryEntry[];
@state() protected dirty = false;
@@ -234,7 +235,7 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
goBack("/config");
return;
}
const entity = this.entityRegistry.find(
const entity = this.entityRegistry?.find(
(ent) => ent.platform === domain && ent.unique_id === id
);
if (entity) {

View File

@@ -15,7 +15,7 @@ import {
extractSearchParam,
removeSearchParam,
} from "../../../common/url/search-params";
import "../../../components/ha-fab";
import "../../../components/ha-button";
import "../../../components/ha-icon-button";
import "../../../components/ha-markdown";
import type { SidebarConfig } from "../../../data/automation";
@@ -112,16 +112,16 @@ export const ManualEditorMixin = <TConfig>(
${this.renderContent()}
</div>
<div class="fab-positioner">
<ha-fab
<ha-button
slot="fab"
size="large"
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this.saving}
extended
@click=${this.saveConfig}
>
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab>
<ha-svg-icon slot="start" .path=${mdiContentSave}></ha-svg-icon>
${this.hass.localize("ui.common.save")}
</ha-button>
</div>
</div>
<div class="sidebar-positioner">

View File

@@ -17,7 +17,6 @@ import { ensureArray } from "../../../common/array/ensure-array";
import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button";
import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
import "../../../components/ha-markdown";
import type {

View File

@@ -21,6 +21,7 @@ import "../../../../components/ha-automation-row";
import type { HaAutomationRow } from "../../../../components/ha-automation-row";
import "../../../../components/ha-card";
import "../../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
@@ -30,7 +31,7 @@ import type {
OptionSidebarConfig,
} from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n";
import { fullEntitiesContext } from "../../../../data/context";
import { fullEntitiesContext } from "../../../../data/context/context";
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type { Action, Option } from "../../../../data/script";
import { showPromptDialog } from "../../../../dialogs/generic/show-dialog-box";
@@ -47,7 +48,6 @@ import {
overflowStyles,
rowStyles,
} from "../styles";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
@customElement("ha-automation-option-row")
export default class HaAutomationOptionRow extends LitElement {

View File

@@ -96,13 +96,14 @@ export const saveFabStyles = css`
:host {
overflow: hidden;
}
ha-fab {
ha-button[slot="fab"] {
position: absolute;
right: calc(16px + var(--safe-area-inset-right, 0px));
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
--ha-button-box-shadow: var(--ha-box-shadow-l);
}
ha-fab.dirty {
ha-button[slot="fab"].dirty {
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
}
`;
@@ -129,14 +130,14 @@ export const manualEditorStyles = css`
justify-content: flex-end;
}
.fab-positioner ha-fab {
.fab-positioner ha-button[slot="fab"] {
position: fixed;
right: unset;
left: unset;
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
.fab-positioner ha-fab.dirty {
.fab-positioner ha-button[slot="fab"].dirty {
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
}

View File

@@ -1,4 +1,4 @@
import { consume } from "@lit/context";
import { consume, type ContextType } from "@lit/context";
import {
mdiAlert,
mdiAlertOctagon,
@@ -16,14 +16,11 @@ import { isTemplate } from "../../../../common/string/has-template";
import "../../../../components/ha-svg-icon";
import type { ConfigEntry } from "../../../../data/config_entries";
import {
areasContext,
configEntriesContext,
devicesContext,
floorsContext,
internationalizationContext,
labelsContext,
localizeContext,
statesContext,
} from "../../../../data/context";
registriesContext,
} from "../../../../data/context/context";
import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
import type { HomeAssistant } from "../../../../types";
import { getTargetIcon } from "./get_target_icon";
@@ -41,24 +38,12 @@ export class HaAutomationRowTargets extends LitElement {
public targetRequired = false;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: HomeAssistant["localize"];
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: floorsContext, subscribe: true })
private floors!: HomeAssistant["floors"];
@state()
@consume({ context: areasContext, subscribe: true })
private areas!: HomeAssistant["areas"];
@state()
@consume({ context: devicesContext, subscribe: true })
private devices!: HomeAssistant["devices"];
@state()
@consume({ context: statesContext, subscribe: true })
private states!: HomeAssistant["states"];
@consume({ context: registriesContext, subscribe: true })
private _registries!: ContextType<typeof registriesContext>;
@state()
@consume({ context: labelsContext, subscribe: true })
@@ -82,7 +67,7 @@ export class HaAutomationRowTargets extends LitElement {
this.targetRequired
? html`<ha-svg-icon .path=${mdiAlertOctagon}></ha-svg-icon>`
: nothing,
this.localize(
this._i18n.localize(
"ui.panel.config.automation.editor.target_summary.no_target"
),
false,
@@ -124,7 +109,7 @@ export class HaAutomationRowTargets extends LitElement {
return html`<span class="target">
<ha-svg-icon .path=${mdiFormatListBulleted}></ha-svg-icon>
<div class="label">
${this.localize(
${this._i18n.localize(
"ui.panel.config.automation.editor.target_summary.targets",
{
count: totalLength,
@@ -142,16 +127,16 @@ export class HaAutomationRowTargets extends LitElement {
targetId: string
): boolean {
if (targetType === "floor") {
return !!this.floors[targetId];
return !!this._registries.floors[targetId];
}
if (targetType === "area") {
return !!this.areas[targetId];
return !!this._registries.areas[targetId];
}
if (targetType === "device") {
return !!this.devices[targetId];
return !!this._registries.devices[targetId];
}
if (targetType === "entity") {
return !!this.states[targetId];
return !!this._registries.entities[targetId];
}
if (targetType === "label") {
return !!this._getLabel(targetId);
@@ -178,7 +163,7 @@ export class HaAutomationRowTargets extends LitElement {
if (targetType === "entity" && ["all", "none"].includes(targetId)) {
return this._renderTargetBadge(
html`<ha-svg-icon .path=${mdiShape}></ha-svg-icon>`,
this.localize(
this._i18n.localize(
`ui.panel.config.automation.editor.target_summary.${targetId as "all" | "none"}_entities`
)
);
@@ -188,7 +173,7 @@ export class HaAutomationRowTargets extends LitElement {
if (isTemplate(targetId)) {
return this._renderTargetBadge(
html`<ha-svg-icon .path=${mdiCodeBraces}></ha-svg-icon>`,
this.localize(
this._i18n.localize(
"ui.panel.config.automation.editor.target_summary.template"
)
);

View File

@@ -39,6 +39,7 @@ import "../../../../components/ha-automation-row";
import type { HaAutomationRow } from "../../../../components/ha-automation-row";
import "../../../../components/ha-card";
import "../../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
@@ -54,7 +55,7 @@ import type {
import { isTrigger, subscribeTrigger } from "../../../../data/automation";
import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import { fullEntitiesContext } from "../../../../data/context/context";
import type { DeviceTrigger } from "../../../../data/device/device_automation";
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type { TriggerDescriptions } from "../../../../data/trigger";
@@ -89,7 +90,6 @@ import "./types/ha-automation-trigger-time";
import "./types/ha-automation-trigger-time_pattern";
import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
export interface TriggerElement extends LitElement {
trigger: Trigger;

View File

@@ -9,7 +9,7 @@ import "../../../../../components/device/ha-device-picker";
import "../../../../../components/device/ha-device-trigger-picker";
import { computeInitialHaFormData } from "../../../../../components/ha-form/compute-initial-ha-form-data";
import "../../../../../components/ha-form/ha-form";
import { fullEntitiesContext } from "../../../../../data/context";
import { fullEntitiesContext } from "../../../../../data/context/context";
import type {
DeviceCapabilities,
DeviceTrigger,

View File

@@ -31,7 +31,6 @@ import type {
HaDropdownSelectEvent,
} from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-filter-states";
import "../../../components/ha-icon";
import "../../../components/ha-icon-next";
@@ -477,24 +476,24 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
></ha-filter-states>
${!this._needsOnboarding
? html`
<ha-fab
<ha-button
slot="fab"
size="large"
?disabled=${backupInProgress}
.label=${this.hass.localize(
"ui.panel.config.backup.backups.new_backup"
)}
extended
@click=${this._newBackup}
>
${backupInProgress
? html`<div slot="icon" class="loading">
? html`<div slot="start" class="loading">
<ha-spinner .size=${"small"}></ha-spinner>
</div>`
: html`<ha-svg-icon
slot="icon"
slot="start"
.path=${mdiPlus}
></ha-svg-icon>`}
</ha-fab>
${this.hass.localize(
"ui.panel.config.backup.backups.new_backup"
)}
</ha-button>
`
: nothing}
</hass-tabs-subpage-data-table>

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