mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-29 17:15:20 +00:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c350d46d44 | ||
|
|
e5a43b48c2 | ||
|
|
5f11dc4580 | ||
|
|
27489f188e | ||
|
|
97fffc3b43 | ||
|
|
a30193b8ef | ||
|
|
cf063dc19f | ||
|
|
5db829b7be | ||
|
|
421f8b1986 | ||
|
|
a140eedc6e | ||
|
|
2933568658 | ||
|
|
29faeaf7fa | ||
|
|
95ba532ceb | ||
|
|
352ecb9d6d | ||
|
|
3f73049a3d | ||
|
|
50ad0c6cd0 |
No files matched your search
@@ -51,8 +51,8 @@
|
||||
|
||||
- This PR fixes or closes issue: fixes #
|
||||
- This PR is related to issue or discussion:
|
||||
- Link to documentation pull request:
|
||||
- Link to developer documentation pull request:
|
||||
- Link to [documentation][docs-repository] pull request:
|
||||
- Link to [contributors documentation][contributors-repository] pull request:
|
||||
- Link to backend pull request:
|
||||
|
||||
## Checklist
|
||||
@@ -63,7 +63,8 @@
|
||||
for before merging your code.
|
||||
|
||||
AI tools are welcome, but contributors are responsible for *fully*
|
||||
understanding the code before submitting a PR.
|
||||
understanding the code before submitting a PR. Please follow our AI policy:
|
||||
https://contributors.home-assistant.io/start/ai-policy/
|
||||
-->
|
||||
|
||||
- [ ] I understand the code I am submitting and can explain how it works.
|
||||
@@ -105,4 +106,5 @@ To help with the load of incoming pull requests:
|
||||
Below, some useful links you could explore:
|
||||
-->
|
||||
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
||||
[contributors-repository]: https://github.com/home-assistant/contributors.home-assistant.io
|
||||
[perfect-pr]: https://contributors.home-assistant.io/start/pull-requests/#before-you-open-it
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list";
|
||||
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
|
||||
import type { Auth, Connection } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
@@ -215,7 +214,7 @@ class HcCast extends LitElement {
|
||||
this.castManager.requestSession();
|
||||
}
|
||||
|
||||
private async _handlePickView(ev: CustomEvent<ActionDetail>) {
|
||||
private async _handlePickView(ev: CustomEvent<{ index: number }>) {
|
||||
const path = this.lovelaceViews?.[ev.detail.index]?.path ?? ev.detail.index;
|
||||
await ensureConnectedCastSession(this.castManager!, this.auth!);
|
||||
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
|
||||
|
||||
+2
-2
@@ -101,7 +101,7 @@
|
||||
"fullcalendar": "7.1.3",
|
||||
"fuse.js": "7.5.0",
|
||||
"hls.js": "1.7.3",
|
||||
"home-assistant-js-websocket": "9.8.0",
|
||||
"home-assistant-js-websocket": "9.8.1",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "12.1.4",
|
||||
"js-yaml": "5.5.0",
|
||||
@@ -110,7 +110,7 @@
|
||||
"lit": "3.3.3",
|
||||
"lit-html": "3.3.3",
|
||||
"maplibre-gl": "6.16.0",
|
||||
"marked": "18.1.1",
|
||||
"marked": "18.1.2",
|
||||
"memoize-one": "6.0.0",
|
||||
"nanoid": "6.0.2",
|
||||
"node-vibrant": "4.0.4",
|
||||
|
||||
Generated
+10
-10
@@ -364,8 +364,8 @@ importers:
|
||||
specifier: 1.7.3
|
||||
version: 1.7.3
|
||||
home-assistant-js-websocket:
|
||||
specifier: 9.8.0
|
||||
version: 9.8.0
|
||||
specifier: 9.8.1
|
||||
version: 9.8.1
|
||||
idb-keyval:
|
||||
specifier: 6.3.0
|
||||
version: 6.3.0
|
||||
@@ -391,8 +391,8 @@ importers:
|
||||
specifier: 6.16.0
|
||||
version: 6.16.0
|
||||
marked:
|
||||
specifier: 18.1.1
|
||||
version: 18.1.1
|
||||
specifier: 18.1.2
|
||||
version: 18.1.2
|
||||
memoize-one:
|
||||
specifier: 6.0.0
|
||||
version: 6.0.0
|
||||
@@ -5205,8 +5205,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-MsPlx6yVW4Qv4C7mEVou4/gk/5cN2dVxOTnrNjPSmyH2f0Ln+/UkG10Ax1PE6OvrmTVPKgQYozTtBaPOuCzq7A==}
|
||||
|
||||
[email protected].0:
|
||||
resolution: {integrity: sha512-Nl5asoKQJwJ5a8N0U72jkSAUdO+hsY/T+Upp0VaZMUSt6A2gtdyvKfBIEYbpnYK3NP83Q/YGaC5XYkO5c08Img==}
|
||||
[email protected].1:
|
||||
resolution: {integrity: sha512-WvovbrSoYLTKfh6aY5SxSWPPy50/mLWLCihQL8G2N+2e3mTrg3QTQCAp4R32/LwbkRBDGj0XNs6JFj8e2IYEUw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
|
||||
@@ -5907,8 +5907,8 @@ packages:
|
||||
engines: {node: '>= 18'}
|
||||
hasBin: true
|
||||
|
||||
[email protected].1:
|
||||
resolution: {integrity: sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg==}
|
||||
[email protected].2:
|
||||
resolution: {integrity: sha512-DfOnvLAQ6WYeWxWO1u4RyFOeqyHup6dks5PPqnoFiooU7ipRvidO/2j8plhAHIiZg574E9ZlzSjUiPMLAzFkgg==}
|
||||
engines: {node: '>= 20'}
|
||||
hasBin: true
|
||||
|
||||
@@ -12962,7 +12962,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected].0: {}
|
||||
[email protected].1: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
@@ -13689,7 +13689,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected].1: {}
|
||||
[email protected].2: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-icon-next";
|
||||
import "../components/ha-list";
|
||||
import "../components/ha-list-item";
|
||||
import "../components/item/ha-list-item-button";
|
||||
import "../components/list/ha-list-base";
|
||||
import type { AuthProvider } from "../data/auth";
|
||||
|
||||
declare global {
|
||||
@@ -32,21 +32,19 @@ export class HaPickAuthProvider extends LitElement {
|
||||
>${this._localize("ui.panel.page-authorize.pick_auth_provider")}</span
|
||||
>
|
||||
</h3>
|
||||
<ha-list>
|
||||
<ha-list-base>
|
||||
${this.authProviders.map(
|
||||
(provider) => html`
|
||||
<ha-list-item
|
||||
hasMeta
|
||||
role="button"
|
||||
<ha-list-item-button
|
||||
.auth_provider=${provider}
|
||||
@click=${this._handlePick}
|
||||
>
|
||||
${provider.name}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
<span slot="headline">${provider.name}</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-base>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -80,9 +78,9 @@ export class HaPickAuthProvider extends LitElement {
|
||||
background: var(--card-background-color);
|
||||
padding: 0 15px;
|
||||
}
|
||||
ha-list {
|
||||
ha-list-base {
|
||||
margin: 16px -16px 0;
|
||||
--mdc-list-side-padding: 24px;
|
||||
--ha-row-item-padding-inline: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ const RASTER_TILE_URL = `${MAP_TILES_PATH}/raster/{z}/{x}/{y}.png?token={token}`
|
||||
// policy strips again unless the tiles ask for it back.
|
||||
const DEMO_RASTER_TILE_URL = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||||
const OSM_ATTRIBUTION =
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
|
||||
'<a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>';
|
||||
|
||||
// Browsers keep about 16 live WebGL contexts and drop the oldest, which a
|
||||
// dashboard full of map cards hits. A transient loss is restored, hence a grace.
|
||||
|
||||
@@ -89,6 +89,9 @@ export const DEFAULT_CHART_WIDTH = 500;
|
||||
// is short enough for a whole screenful; on a desktop that would cover the page.
|
||||
const VISIBILITY_ROOT_MARGIN_NARROW = "100%";
|
||||
const VISIBILITY_ROOT_MARGIN = "300px";
|
||||
const CLICK_DRAG_TOLERANCE = 4;
|
||||
|
||||
export type ZoomSliderMode = "auto" | "always" | "never";
|
||||
const DEFERRED_PROPS = [
|
||||
"options",
|
||||
"data",
|
||||
@@ -163,15 +166,20 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
@property({ attribute: "small-controls", type: Boolean })
|
||||
public smallControls?: boolean;
|
||||
|
||||
// For charts zoomed from outside, like the synced History charts
|
||||
@property({ attribute: "hide-zoom-controls", type: Boolean })
|
||||
public hideZoomControls = false;
|
||||
// "auto" shows the zoom slider once zoomed. "never" hides every zoom
|
||||
// control, for charts zoomed from outside like the synced History charts.
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
|
||||
// For charts sized to their content, like timelines, which grow to fit the
|
||||
// zoom slider instead of giving up plot height
|
||||
@property({ attribute: "grow-for-zoom-slider", type: Boolean })
|
||||
public growForZoomSlider = false;
|
||||
|
||||
// Dragging selects a time range instead of zooming
|
||||
@property({ attribute: "select-range", type: Boolean })
|
||||
public selectRange = false;
|
||||
|
||||
// extraComponents is not reactive and should not trigger updates
|
||||
public extraComponents?: any[];
|
||||
|
||||
@@ -197,6 +205,8 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
|
||||
@state() private _isZoomed = false;
|
||||
|
||||
private _pointerDownX?: number;
|
||||
|
||||
// Separate from _isZoomed, whose touch handling reruns graph layouts
|
||||
@state() private _isGraphRoamed = false;
|
||||
|
||||
@@ -450,6 +460,9 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("selectRange")) {
|
||||
this._updateSelectRange();
|
||||
}
|
||||
const themeChanged = changedProps.has("_themes") && this.hasUpdated;
|
||||
if (!themeChanged && !DEFERRED_PROPS.some((p) => changedProps.has(p))) {
|
||||
return;
|
||||
@@ -617,7 +630,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
>
|
||||
${
|
||||
(this._isZoomed || this._isGraphRoamed) &&
|
||||
!this.hideZoomControls &&
|
||||
this.zoomSlider !== "never" &&
|
||||
!showZoomSlider
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
@@ -901,6 +914,32 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
this.chart.on("click", (e: ECElementEvent) => {
|
||||
fireEvent(this, "chart-click", e);
|
||||
});
|
||||
this.chart.getZr().on("mousedown", (e: ECElementEvent) => {
|
||||
this._pointerDownX = e.offsetX;
|
||||
});
|
||||
this.chart.getZr().on("click", (e: ECElementEvent) => {
|
||||
const point = [e.offsetX, e.offsetY];
|
||||
// A drag that ends on the plot is not a click
|
||||
const dragged =
|
||||
this._pointerDownX !== undefined &&
|
||||
Math.abs(e.offsetX - this._pointerDownX) > CLICK_DRAG_TOLERANCE;
|
||||
if (dragged || !this.chart?.containPixel("grid", point)) {
|
||||
return;
|
||||
}
|
||||
const [x, y] = this.chart.convertFromPixel("grid", point) as number[];
|
||||
fireEvent(this, "chart-grid-click", { x, y });
|
||||
});
|
||||
this.chart.on("brushEnd", (e: any) => {
|
||||
const range = e.areas?.[0]?.coordRange as number[] | undefined;
|
||||
// The caller shows the selection, snapped to its data
|
||||
this.chart?.dispatchAction({ type: "brush", areas: [] });
|
||||
if (range) {
|
||||
fireEvent(this, "chart-range-select", {
|
||||
start: range[0],
|
||||
end: range[1],
|
||||
});
|
||||
}
|
||||
});
|
||||
this.chart.on("sankeyroam", () => {
|
||||
const option = this.chart!.getOption();
|
||||
const series = option.series as any[];
|
||||
@@ -966,6 +1005,9 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
this._legendItemCount = this._getLegendItems()?.length;
|
||||
this._updateSankeyRoam();
|
||||
this._updateDragToZoom();
|
||||
if (this.selectRange) {
|
||||
this._updateSelectRange();
|
||||
}
|
||||
if (zoomStart !== 0 || zoomEnd !== 100) {
|
||||
this.zoom(zoomStart, zoomEnd, true);
|
||||
}
|
||||
@@ -1017,7 +1059,8 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
|
||||
// A chart can switch type (e.g. pie and bar) without being rebuilt
|
||||
private _updateDragToZoom() {
|
||||
const active = !this._touchInput && this._hasZoomableXAxis();
|
||||
const active =
|
||||
!this._touchInput && this._hasZoomableXAxis() && !this.selectRange;
|
||||
if (!this.chart || active === this._dragToZoom) {
|
||||
return;
|
||||
}
|
||||
@@ -1029,6 +1072,33 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _updateSelectRange() {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
if (this.selectRange) {
|
||||
this.chart.setOption({
|
||||
brush: { xAxisIndex: 0, transformable: false },
|
||||
});
|
||||
}
|
||||
const brushCursor = {
|
||||
type: "takeGlobalCursor",
|
||||
key: "brush",
|
||||
brushOption: {
|
||||
brushType: this.selectRange ? "lineX" : false,
|
||||
brushMode: "single",
|
||||
},
|
||||
};
|
||||
// Brush and drag to zoom share the global cursor, the last one taken wins
|
||||
if (this.selectRange) {
|
||||
this._updateDragToZoom();
|
||||
this.chart.dispatchAction(brushCursor);
|
||||
} else {
|
||||
this.chart.dispatchAction(brushCursor);
|
||||
this._updateDragToZoom();
|
||||
}
|
||||
}
|
||||
|
||||
private _hasZoomableXAxis() {
|
||||
const xAxis = ensureArray(this.options?.xAxis)?.[0] as
|
||||
XAXisOption | undefined;
|
||||
@@ -1064,9 +1134,12 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
private _showZoomSlider() {
|
||||
const [start, end] = this._zoomRange;
|
||||
return (
|
||||
!this.hideZoomControls &&
|
||||
this.zoomSlider !== "never" &&
|
||||
this._hasZoomableXAxis() &&
|
||||
(this._zoomSliderHeld || start !== 0 || end !== 100)
|
||||
(this.zoomSlider === "always" ||
|
||||
this._zoomSliderHeld ||
|
||||
start !== 0 ||
|
||||
end !== 100)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1532,7 +1605,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _handleClickZoom = (e: ECElementEvent) => {
|
||||
if (!this.chart) {
|
||||
if (!this.chart || this.selectRange) {
|
||||
return;
|
||||
}
|
||||
// Handle sankey chart double-click zoom
|
||||
@@ -1745,7 +1818,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
// it, so a quick drag out would lose the release and its selection
|
||||
if (
|
||||
ev.type === "pointerdown" &&
|
||||
this._dragToZoom &&
|
||||
(this._dragToZoom || this.selectRange) &&
|
||||
this._isZoomPress(ev)
|
||||
) {
|
||||
this.chart
|
||||
@@ -2317,6 +2390,8 @@ declare global {
|
||||
"dataset-hidden": { id: string };
|
||||
"dataset-unhidden": { id: string };
|
||||
"chart-click": ECElementEvent;
|
||||
"chart-grid-click": { x: number; y: number };
|
||||
"chart-range-select": { start: number; end: number };
|
||||
"legend-label-click": { id: string };
|
||||
"chart-zoom": {
|
||||
start: number;
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { ZoomSliderMode } from "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import {
|
||||
@@ -105,8 +106,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
@property({ attribute: "expand-legend", type: Boolean })
|
||||
public expandLegend?: boolean;
|
||||
|
||||
@property({ attribute: "hide-zoom-controls", type: Boolean })
|
||||
public hideZoomControls = false;
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
|
||||
@state() private _chartData: LineSeriesOption[] = [];
|
||||
|
||||
@@ -159,7 +160,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
@chart-zoom=${this._handleDataZoom}
|
||||
.expandLegend=${this.expandLegend}
|
||||
.hideZoomControls=${this.hideZoomControls}
|
||||
.zoomSlider=${this.zoomSlider}
|
||||
.clickLabelForMoreInfo=${this.clickForMoreInfo}
|
||||
@legend-label-click=${this._handleLegendLabelClick}
|
||||
></ha-chart-base>
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../types";
|
||||
import { DEFAULT_CHART_WIDTH, MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { ZoomSliderMode } from "./ha-chart-base";
|
||||
import { itemTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import type { HaECOption, HaECSeries } from "../../resources/echarts/echarts";
|
||||
@@ -68,8 +69,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public chartIndex?;
|
||||
|
||||
@property({ attribute: "hide-zoom-controls", type: Boolean })
|
||||
public hideZoomControls = false;
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
|
||||
@state() private _chartData: CustomSeriesOption[] = [];
|
||||
|
||||
@@ -105,7 +106,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
grow-for-zoom-slider
|
||||
@chart-click=${this._handleChartClick}
|
||||
@chart-zoom=${this._handleDataZoom}
|
||||
.hideZoomControls=${this.hideZoomControls}
|
||||
.zoomSlider=${this.zoomSlider}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
@chart-zoom-with-index=${this._handleTimelineSync}
|
||||
.height=${this.virtualize ? undefined : this.height}
|
||||
.expandLegend=${this.expandLegend}
|
||||
?hide-zoom-controls=${this.syncCharts}
|
||||
.zoomSlider=${this.syncCharts ? "never" : "auto"}
|
||||
></state-history-chart-line>
|
||||
</div> `;
|
||||
}
|
||||
@@ -237,7 +237,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
.clickForMoreInfo=${this.clickForMoreInfo}
|
||||
@y-width-changed=${this._yWidthChanged}
|
||||
@chart-zoom-with-index=${this._handleTimelineSync}
|
||||
?hide-zoom-controls=${this.syncCharts}
|
||||
.zoomSlider=${this.syncCharts ? "never" : "auto"}
|
||||
></state-history-chart-timeline>
|
||||
</div> `;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
import type { HassConfig, HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } 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 { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
@@ -49,7 +49,11 @@ import type {
|
||||
HomeAssistantUI,
|
||||
} from "../../types";
|
||||
import { getPeriodicAxisLabelConfig } from "./axis-label";
|
||||
import type { CustomLegendOption } from "./ha-chart-base";
|
||||
import type {
|
||||
CustomLegendOption,
|
||||
HaChartBase,
|
||||
ZoomSliderMode,
|
||||
} from "./ha-chart-base";
|
||||
import "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
@@ -128,6 +132,19 @@ export class StatisticsChart extends LitElement {
|
||||
@property({ attribute: "expand-legend", type: Boolean })
|
||||
public expandLegend?: boolean;
|
||||
|
||||
@property({ attribute: "select-range", type: Boolean })
|
||||
public selectRange = false;
|
||||
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
|
||||
@property({ attribute: false }) public highlight?: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
|
||||
@query("ha-chart-base") private _chartBase?: HaChartBase;
|
||||
|
||||
@state() private _chartData: (LineSeriesOption | BarSeriesOption)[] = [];
|
||||
|
||||
@state() private _legendData: CustomLegendOption["data"];
|
||||
@@ -243,9 +260,15 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-chart-base
|
||||
.data=${this._chartData}
|
||||
.data=${this._highlightedChartData(
|
||||
this._chartData,
|
||||
this.highlight,
|
||||
getComputedStyle(this).getPropertyValue("--primary-color").trim()
|
||||
)}
|
||||
.options=${this._chartOptions}
|
||||
.height=${this.height}
|
||||
.selectRange=${this.selectRange}
|
||||
.zoomSlider=${this.zoomSlider}
|
||||
style=${styleMap({ height: this.height })}
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
@@ -259,6 +282,59 @@ export class StatisticsChart extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public resetZoom() {
|
||||
this._chartBase?.zoom(0, 100);
|
||||
}
|
||||
|
||||
private _highlightedChartData = memoizeOne(
|
||||
(
|
||||
data: (LineSeriesOption | BarSeriesOption)[],
|
||||
highlight: { start: number; end: number } | undefined,
|
||||
color: string
|
||||
): (LineSeriesOption | BarSeriesOption)[] => {
|
||||
if (!data.length) {
|
||||
return data;
|
||||
}
|
||||
if (data[0].type === "bar") {
|
||||
if (!highlight) {
|
||||
return data;
|
||||
}
|
||||
// Bars outside the highlight fade, so the highlighted ones stand out.
|
||||
// Some points are already objects, like negative bars with round caps.
|
||||
return data.map((series) => ({
|
||||
...series,
|
||||
data: (series.data as any[]).map((item) => {
|
||||
const point =
|
||||
item && typeof item === "object" && "value" in item
|
||||
? item
|
||||
: { value: item };
|
||||
// Current state bars only carry their time
|
||||
const start = new Date(point.value[2] ?? point.value[0]).getTime();
|
||||
return start >= highlight.start && start < highlight.end
|
||||
? item
|
||||
: {
|
||||
...point,
|
||||
itemStyle: { ...point.itemStyle, opacity: 0.3 },
|
||||
};
|
||||
}),
|
||||
})) as BarSeriesOption[];
|
||||
}
|
||||
// Every series gets an area, empty when not highlighted, as the chart
|
||||
// merges series and would keep an old one
|
||||
return data.map((series, index) => ({
|
||||
...series,
|
||||
markArea: {
|
||||
silent: true,
|
||||
itemStyle: { color, opacity: 0.15 },
|
||||
data:
|
||||
highlight && index === 0
|
||||
? [[{ xAxis: highlight.start }, { xAxis: highlight.end }]]
|
||||
: [],
|
||||
},
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
private _datasetHidden(ev: HASSDomEvent<HASSDomEvents["dataset-hidden"]>) {
|
||||
this._hiddenStats.add(ev.detail.id);
|
||||
this.requestUpdate("_hiddenStats");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import 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";
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
@@ -32,8 +32,8 @@ import type { HaBaseTimeInput } from "../ha-base-time-input";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-icon-button-next";
|
||||
import "../ha-icon-button-prev";
|
||||
import "../ha-list";
|
||||
import "../ha-list-item";
|
||||
import "../item/ha-list-item-button";
|
||||
import "../list/ha-list-base";
|
||||
import "../ha-time-input";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
import type { DateRangePickerRanges } from "./ha-date-range-picker";
|
||||
@@ -166,11 +166,19 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-list @action=${this._setDateRange} activatable>
|
||||
${Object.keys(this.ranges!).map(
|
||||
(name) => html`<ha-list-item>${name}</ha-list-item>`
|
||||
<ha-list-base>
|
||||
${Object.entries(this.ranges!).map(
|
||||
([name, range], index) => html`
|
||||
<ha-list-item-button
|
||||
.range=${range}
|
||||
.index=${index}
|
||||
@click=${this._setDateRange}
|
||||
>
|
||||
<span slot="headline">${name}</span>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-base>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -343,11 +351,12 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
this._saveDateRangePreset(chip.range, chip.index);
|
||||
}
|
||||
|
||||
private _setDateRange(ev: HASSDomEvent<ActionDetail>) {
|
||||
const dateRange: [Date, Date] = Object.values(this.ranges!)[
|
||||
ev.detail.index
|
||||
];
|
||||
this._saveDateRangePreset(dateRange, ev.detail.index);
|
||||
private _setDateRange(
|
||||
ev: HASSDomCurrentTargetEvent<
|
||||
HTMLElement & { range: [Date, Date]; index: number }
|
||||
>
|
||||
) {
|
||||
this._saveDateRangePreset(ev.currentTarget.range, ev.currentTarget.index);
|
||||
}
|
||||
|
||||
private _saveDateRangePreset(range: [Date, Date], index: number) {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { css } from "lit";
|
||||
import { RadioListItemBase } from "@material/mwc-list/mwc-radio-list-item-base";
|
||||
import { styles as controlStyles } from "@material/mwc-list/mwc-control-list-item.css";
|
||||
import { styles } from "@material/mwc-list/mwc-list-item.css";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-radio-list-item")
|
||||
export class HaRadioListItem extends RadioListItemBase {
|
||||
async onChange(event) {
|
||||
super.onChange(event);
|
||||
fireEvent(this, event.type);
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
controlStyles,
|
||||
css`
|
||||
:host {
|
||||
--mdc-theme-secondary: var(--primary-color);
|
||||
}
|
||||
|
||||
:host([graphic="avatar"]) .mdc-deprecated-list-item__graphic,
|
||||
:host([graphic="medium"]) .mdc-deprecated-list-item__graphic,
|
||||
:host([graphic="large"]) .mdc-deprecated-list-item__graphic,
|
||||
:host([graphic="control"]) .mdc-deprecated-list-item__graphic {
|
||||
margin-inline-end: var(--mdc-list-item-graphic-margin, 16px);
|
||||
margin-inline-start: 0px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-deprecated-list-item__meta {
|
||||
flex-shrink: 0;
|
||||
direction: var(--direction);
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
.mdc-deprecated-list-item__graphic {
|
||||
margin-top: var(--radio-list-item-graphic-margin-top);
|
||||
}
|
||||
:host([graphic="icon"]) .mdc-deprecated-list-item__graphic {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: var(--mdc-list-item-graphic-margin, 32px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-radio-list-item": HaRadioListItem;
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,10 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/item/ha-list-item-base";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import "../../../components/user/ha-person-badge";
|
||||
import type { Person } from "../../../data/person";
|
||||
import {
|
||||
@@ -92,23 +93,22 @@ export class HaConfigPerson extends LitElement {
|
||||
</span>
|
||||
|
||||
<ha-card outlined class="storage">
|
||||
<ha-list>
|
||||
<ha-list-base>
|
||||
${this._storageItems.map(
|
||||
(entry) => html`
|
||||
<ha-list-item
|
||||
graphic="avatar"
|
||||
<ha-list-item-button
|
||||
@click=${this._openEditEntry}
|
||||
.entry=${entry}
|
||||
>
|
||||
<ha-person-badge
|
||||
.person=${entry}
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
></ha-person-badge>
|
||||
<span>${entry.name}</span>
|
||||
</ha-list-item>
|
||||
<span slot="headline">${entry.name}</span>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-base>
|
||||
${
|
||||
this._storageItems.length === 0
|
||||
? html`
|
||||
@@ -138,19 +138,19 @@ export class HaConfigPerson extends LitElement {
|
||||
this._configItems.length > 0
|
||||
? html`
|
||||
<ha-card outlined header="Configuration.yaml persons">
|
||||
<ha-list>
|
||||
<ha-list-base>
|
||||
${this._configItems.map(
|
||||
(entry) => html`
|
||||
<ha-list-item graphic="avatar">
|
||||
<ha-list-item-base>
|
||||
<ha-person-badge
|
||||
.person=${entry}
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
></ha-person-badge>
|
||||
<span>${entry.name}</span>
|
||||
</ha-list-item>
|
||||
<span slot="headline">${entry.name}</span>
|
||||
</ha-list-item-base>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
`
|
||||
: nothing
|
||||
@@ -299,7 +299,7 @@ export class HaConfigPerson extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
ha-list:has(+ .empty) {
|
||||
ha-list-base:has(+ .empty) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -73,6 +73,10 @@ const mountPartitionLabel = ({
|
||||
return drive ? `${drive} — ${identity}, ${size}` : `${identity}, ${size}`;
|
||||
};
|
||||
|
||||
// Supervisor accepts only ASCII letters, digits and underscores.
|
||||
const mountNameFromLabel = (label?: string): string =>
|
||||
(label ?? "").replace(/^\W+|\W+$/g, "").replace(/\W+/g, "_");
|
||||
|
||||
const requiredFieldsFilled = (
|
||||
schema: readonly HaFormSchema[],
|
||||
data?: MountFormData
|
||||
@@ -305,6 +309,8 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
|
||||
private _originalType?: SupervisorMountType;
|
||||
|
||||
private _suggestedName?: string;
|
||||
|
||||
@state() private _diskIdentity?: string;
|
||||
|
||||
public connectedCallback(): void {
|
||||
@@ -529,7 +535,9 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
this._validationError = {};
|
||||
this._validationWarning = {};
|
||||
const previous = this._data;
|
||||
this._data = ev.detail.value;
|
||||
this._suggestName(previous);
|
||||
if (this._data?.name && !/^\w+$/.test(this._data.name)) {
|
||||
this._validationError.name = "invalid_name";
|
||||
}
|
||||
@@ -556,6 +564,32 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
);
|
||||
}
|
||||
|
||||
// Name a new disk mount after the partition's volume label, unless the user
|
||||
// typed a name of their own.
|
||||
private _suggestName(previous?: MountFormData) {
|
||||
const data = this._data;
|
||||
if (data?.name !== previous?.name) {
|
||||
this._suggestedName = undefined;
|
||||
}
|
||||
if (
|
||||
this._existing ||
|
||||
data?.type !== SupervisorMountType.DISK ||
|
||||
(data.device === previous?.device &&
|
||||
previous?.type === SupervisorMountType.DISK) ||
|
||||
(data.name && data.name !== this._suggestedName)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const name = mountNameFromLabel(
|
||||
this._partitions?.find((entry) => entry.partition.device === data.device)
|
||||
?.partition.label
|
||||
);
|
||||
if (name !== (data.name ?? "")) {
|
||||
data.name = name;
|
||||
}
|
||||
this._suggestedName = name;
|
||||
}
|
||||
|
||||
private async _connectMount(ev) {
|
||||
const progressButton = ev.target as HaProgressButton;
|
||||
this._error = undefined;
|
||||
|
||||
@@ -28,7 +28,7 @@ import type { ContextType } from "@lit/context";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
@@ -78,7 +78,6 @@ import type {
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../../types";
|
||||
import { animationStyles } from "../../../resources/theme/animations.globals";
|
||||
|
||||
const RANGE_KEYS: DateRange[] = [
|
||||
"today",
|
||||
@@ -151,6 +150,10 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _loadingLeaving = false;
|
||||
|
||||
@query(".loading-indicator") private _loadingIndicator?: HTMLElement;
|
||||
|
||||
private _loadingTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
@@ -379,10 +382,15 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._loading
|
||||
this._loading || this._loadingLeaving
|
||||
? html`<ha-spinner
|
||||
class="loading-indicator"
|
||||
class=${classMap({
|
||||
"loading-indicator": true,
|
||||
leaving: this._loadingLeaving,
|
||||
})}
|
||||
size="small"
|
||||
@transitionend=${this._loadingTransitionDone}
|
||||
@transitioncancel=${this._loadingTransitionDone}
|
||||
></ha-spinner>`
|
||||
: nothing
|
||||
}
|
||||
@@ -727,6 +735,10 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
|
||||
private _updateDates(energyData: EnergyData): void {
|
||||
clearTimeout(this._loadingTimer);
|
||||
if (this._loading) {
|
||||
this._loadingLeaving = true;
|
||||
this._removeLoadingIndicatorWithoutTransition();
|
||||
}
|
||||
this._loading = false;
|
||||
this._compare = energyData.startCompare !== undefined;
|
||||
this._startDate = energyData.start;
|
||||
@@ -759,10 +771,30 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
// Basically to ensure there's no "flash loading" when data is loaded quickly
|
||||
clearTimeout(this._loadingTimer);
|
||||
this._loadingTimer = setTimeout(() => {
|
||||
this._loadingLeaving = false;
|
||||
this._loading = true;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
private async _removeLoadingIndicatorWithoutTransition() {
|
||||
await this.updateComplete;
|
||||
// No transition starts when the spinner was still at opacity 0
|
||||
this._removeIdleLoadingIndicator();
|
||||
}
|
||||
|
||||
private _loadingTransitionDone(ev: TransitionEvent) {
|
||||
if (ev.propertyName === "opacity") {
|
||||
this._removeIdleLoadingIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
private _removeIdleLoadingIndicator() {
|
||||
// Reversing the fade-in cancels a transition while the fade-out runs
|
||||
if (!this._loadingIndicator?.getAnimations().length) {
|
||||
this._loadingLeaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _getDatePickerPlacement = memoizeOne(
|
||||
(
|
||||
verticalOpeningDirection: VerticalOpeningDirection | undefined,
|
||||
@@ -778,7 +810,6 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
);
|
||||
|
||||
static styles = css`
|
||||
${animationStyles}
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -843,7 +874,15 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
position: absolute;
|
||||
right: var(--ha-space-2);
|
||||
align-items: center;
|
||||
animation: fade-in var(--ha-animation-duration-normal) ease-in-out;
|
||||
transition: opacity var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
@starting-style {
|
||||
.loading-indicator {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.loading-indicator.leaving {
|
||||
opacity: 0;
|
||||
}
|
||||
.date-actions .overflow {
|
||||
display: flex;
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
DataZoomComponent,
|
||||
VisualMapComponent,
|
||||
ToolboxComponent,
|
||||
MarkAreaComponent,
|
||||
BrushComponent,
|
||||
} from "echarts/components";
|
||||
|
||||
// Features like Universal Transition and Label Layout
|
||||
@@ -48,6 +50,8 @@ import type {
|
||||
GridComponentOption,
|
||||
DataZoomComponentOption,
|
||||
VisualMapComponentOption,
|
||||
MarkAreaComponentOption,
|
||||
BrushComponentOption,
|
||||
} from "echarts/components";
|
||||
import type { ComposeOption } from "echarts/core";
|
||||
|
||||
@@ -62,6 +66,8 @@ export type ECOption = ComposeOption<
|
||||
| GridComponentOption
|
||||
| DataZoomComponentOption
|
||||
| VisualMapComponentOption
|
||||
| MarkAreaComponentOption
|
||||
| BrushComponentOption
|
||||
| SankeySeriesOption
|
||||
| GraphSeriesOption
|
||||
| SunburstSeriesOption
|
||||
@@ -91,6 +97,8 @@ echarts.use([
|
||||
UniversalTransition,
|
||||
CanvasRenderer,
|
||||
ToolboxComponent,
|
||||
MarkAreaComponent,
|
||||
BrushComponent,
|
||||
]);
|
||||
|
||||
export { LinearGradient };
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Install the Intl.PluralRules polyfill together with its locale data.
|
||||
// FormatJS replaces Intl.PluralRules as soon as polyfill-force runs, and a
|
||||
// data-less polyfill throws from select(). Keeping both imports in one module
|
||||
// closes that gap for every caller, including the should-polyfill probes
|
||||
// that other entry bundles run.
|
||||
import "@formatjs/intl-pluralrules/polyfill-force";
|
||||
// Locale data for plural rules breaks current JSON conversions as it includes functions,
|
||||
// so only import English to avoid huge bundles
|
||||
// TODO: Setup JS imports instead of JSON fetches
|
||||
import "@formatjs/intl-pluralrules/locale-data/en";
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
polyfillTimeZoneData,
|
||||
} from "./locale-data-polyfill";
|
||||
|
||||
let polyfilled = false;
|
||||
|
||||
const _polyfillTimeZoneData = polyfillTimeZoneData;
|
||||
|
||||
// The DateTimeFormat and DurationFormat polyfills call BigInt() when formatting
|
||||
@@ -47,10 +45,6 @@ const setDefaultTimeZone = () => {
|
||||
};
|
||||
|
||||
const polyfillIntl = async () => {
|
||||
if (polyfilled) {
|
||||
return;
|
||||
}
|
||||
polyfilled = true;
|
||||
const locale = getLocalLanguage();
|
||||
const polyfills: Promise<unknown>[] = [];
|
||||
if (shouldPolyfillGetCanonicalLocales()) {
|
||||
@@ -79,14 +73,10 @@ const polyfillIntl = async () => {
|
||||
polyfills.push(import("@formatjs/intl-numberformat/polyfill-force"));
|
||||
}
|
||||
if (shouldPolyfillPluralRules(locale)) {
|
||||
polyfills.push(
|
||||
import("@formatjs/intl-pluralrules/polyfill-force").then(
|
||||
// Locale data for plural rules breaks current JSON conversions as it includes functions,
|
||||
// so only import English to avoid huge bundles
|
||||
// TODO: Setup JS imports instead of JSON fetches
|
||||
() => import("@formatjs/intl-pluralrules/locale-data/en")
|
||||
)
|
||||
);
|
||||
// The polyfill and its English data are one chunk, so Intl.PluralRules
|
||||
// is never installed without data. Its should-polyfill probe calls
|
||||
// select(), which throws on a data-less polyfill.
|
||||
polyfills.push(import("./intl-pluralrules"));
|
||||
}
|
||||
if (shouldPolyfillRelativeTimeFormat(locale)) {
|
||||
polyfills.push(import("@formatjs/intl-relativetimeformat/polyfill-force"));
|
||||
@@ -100,4 +90,9 @@ const polyfillIntl = async () => {
|
||||
);
|
||||
};
|
||||
|
||||
await polyfillIntl();
|
||||
// Each legacy entry bundle (core, app, authorize, onboarding) carries its own
|
||||
// copy of this module, and the bundles load in parallel. A module-level flag
|
||||
// only stops re-runs within one bundle, so a second bundle could probe
|
||||
// Intl.* while the first was still loading locale data. Share one promise
|
||||
// per page instead, so every bundle waits for the same run.
|
||||
await (globalThis.__haIntlPolyfill ??= polyfillIntl());
|
||||
@@ -41,6 +41,8 @@ declare global {
|
||||
var __HASS_URL__: string;
|
||||
// Read by intl-polyfill before a polyfill replaces Intl.DateTimeFormat
|
||||
var __haNativeTimeZone: string | undefined;
|
||||
// Shared by every legacy entry bundle so the Intl polyfills load once
|
||||
var __haIntlPolyfill: Promise<void> | undefined;
|
||||
// Handshake between the app entry and the extra modules in index.html
|
||||
var __haScopedRegistryReady: boolean | undefined;
|
||||
var __haLoadExtraModules: (() => void) | undefined;
|
||||
|
||||
Reference in new issue
Block a user