Compare commits

..
Author SHA1 Message Date
Simon Lamon cfcfe4816d ha-media-player-browse migrate list 2026-10-29 15:50:09 +00:00
21 changed files with 184 additions and 390 deletions

No files matched your search

+3 -5
View File
@@ -51,8 +51,8 @@
- This PR fixes or closes issue: fixes #
- This PR is related to issue or discussion:
- Link to [documentation][docs-repository] pull request:
- Link to [contributors documentation][contributors-repository] pull request:
- Link to documentation pull request:
- Link to developer documentation pull request:
- Link to backend pull request:
## Checklist
@@ -63,8 +63,7 @@
for before merging your code.
AI tools are welcome, but contributors are responsible for *fully*
understanding the code before submitting a PR. Please follow our AI policy:
https://contributors.home-assistant.io/start/ai-policy/
understanding the code before submitting a PR.
-->
- [ ] I understand the code I am submitting and can explain how it works.
@@ -106,5 +105,4 @@ 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
+2 -1
View File
@@ -1,3 +1,4 @@
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";
@@ -214,7 +215,7 @@ class HcCast extends LitElement {
this.castManager.requestSession();
}
private async _handlePickView(ev: CustomEvent<{ index: number }>) {
private async _handlePickView(ev: CustomEvent<ActionDetail>) {
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
View File
@@ -101,7 +101,7 @@
"fullcalendar": "7.1.3",
"fuse.js": "7.5.0",
"hls.js": "1.7.3",
"home-assistant-js-websocket": "9.8.1",
"home-assistant-js-websocket": "9.8.0",
"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.2",
"marked": "18.1.1",
"memoize-one": "6.0.0",
"nanoid": "6.0.2",
"node-vibrant": "4.0.4",
+10 -10
View File
@@ -364,8 +364,8 @@ importers:
specifier: 1.7.3
version: 1.7.3
home-assistant-js-websocket:
specifier: 9.8.1
version: 9.8.1
specifier: 9.8.0
version: 9.8.0
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.2
version: 18.1.2
specifier: 18.1.1
version: 18.1.1
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].1:
resolution: {integrity: sha512-WvovbrSoYLTKfh6aY5SxSWPPy50/mLWLCihQL8G2N+2e3mTrg3QTQCAp4R32/LwbkRBDGj0XNs6JFj8e2IYEUw==}
[email protected].0:
resolution: {integrity: sha512-Nl5asoKQJwJ5a8N0U72jkSAUdO+hsY/T+Upp0VaZMUSt6A2gtdyvKfBIEYbpnYK3NP83Q/YGaC5XYkO5c08Img==}
[email protected]:
resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
@@ -5907,8 +5907,8 @@ packages:
engines: {node: '>= 18'}
hasBin: true
[email protected].2:
resolution: {integrity: sha512-DfOnvLAQ6WYeWxWO1u4RyFOeqyHup6dks5PPqnoFiooU7ipRvidO/2j8plhAHIiZg574E9ZlzSjUiPMLAzFkgg==}
[email protected].1:
resolution: {integrity: sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg==}
engines: {node: '>= 20'}
hasBin: true
@@ -12962,7 +12962,7 @@ snapshots:
[email protected]: {}
[email protected].1: {}
[email protected].0: {}
[email protected]:
dependencies:
@@ -13689,7 +13689,7 @@ snapshots:
[email protected]: {}
[email protected].2: {}
[email protected].1: {}
[email protected]: {}
+12 -10
View File
@@ -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/item/ha-list-item-button";
import "../components/list/ha-list-base";
import "../components/ha-list";
import "../components/ha-list-item";
import type { AuthProvider } from "../data/auth";
declare global {
@@ -32,19 +32,21 @@ export class HaPickAuthProvider extends LitElement {
>${this._localize("ui.panel.page-authorize.pick_auth_provider")}</span
>
</h3>
<ha-list-base>
<ha-list>
${this.authProviders.map(
(provider) => html`
<ha-list-item-button
<ha-list-item
hasMeta
role="button"
.auth_provider=${provider}
@click=${this._handlePick}
>
<span slot="headline">${provider.name}</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-button>
${provider.name}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
)}
</ha-list-base>
</ha-list>
`;
}
@@ -78,9 +80,9 @@ export class HaPickAuthProvider extends LitElement {
background: var(--card-background-color);
padding: 0 15px;
}
ha-list-base {
ha-list {
margin: 16px -16px 0;
--ha-row-item-padding-inline: 24px;
--mdc-list-side-padding: 24px;
}
`;
}
+1 -1
View File
@@ -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>';
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
// 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.
+9 -84
View File
@@ -89,9 +89,6 @@ 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",
@@ -166,20 +163,15 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
@property({ attribute: "small-controls", type: Boolean })
public smallControls?: boolean;
// "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 zoomed from outside, like the synced History charts
@property({ attribute: "hide-zoom-controls", type: Boolean })
public hideZoomControls = false;
// 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[];
@@ -205,8 +197,6 @@ 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;
@@ -460,9 +450,6 @@ 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;
@@ -630,7 +617,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
>
${
(this._isZoomed || this._isGraphRoamed) &&
this.zoomSlider !== "never" &&
!this.hideZoomControls &&
!showZoomSlider
? html`<ha-icon-button
class="zoom-reset"
@@ -914,32 +901,6 @@ 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[];
@@ -1005,9 +966,6 @@ 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);
}
@@ -1059,8 +1017,7 @@ 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() && !this.selectRange;
const active = !this._touchInput && this._hasZoomableXAxis();
if (!this.chart || active === this._dragToZoom) {
return;
}
@@ -1072,33 +1029,6 @@ 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;
@@ -1134,12 +1064,9 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
private _showZoomSlider() {
const [start, end] = this._zoomRange;
return (
this.zoomSlider !== "never" &&
!this.hideZoomControls &&
this._hasZoomableXAxis() &&
(this.zoomSlider === "always" ||
this._zoomSliderHeld ||
start !== 0 ||
end !== 100)
(this._zoomSliderHeld || start !== 0 || end !== 100)
);
}
@@ -1605,7 +1532,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
}
private _handleClickZoom = (e: ECElementEvent) => {
if (!this.chart || this.selectRange) {
if (!this.chart) {
return;
}
// Handle sankey chart double-click zoom
@@ -1818,7 +1745,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.selectRange) &&
this._dragToZoom &&
this._isZoomPress(ev)
) {
this.chart
@@ -2390,8 +2317,6 @@ 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,7 +24,6 @@ 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 {
@@ -106,8 +105,8 @@ export class StateHistoryChartLine extends LitElement {
@property({ attribute: "expand-legend", type: Boolean })
public expandLegend?: boolean;
@property({ attribute: "zoom-slider" })
public zoomSlider: ZoomSliderMode = "auto";
@property({ attribute: "hide-zoom-controls", type: Boolean })
public hideZoomControls = false;
@state() private _chartData: LineSeriesOption[] = [];
@@ -160,7 +159,7 @@ export class StateHistoryChartLine extends LitElement {
@dataset-unhidden=${this._datasetUnhidden}
@chart-zoom=${this._handleDataZoom}
.expandLegend=${this.expandLegend}
.zoomSlider=${this.zoomSlider}
.hideZoomControls=${this.hideZoomControls}
.clickLabelForMoreInfo=${this.clickForMoreInfo}
@legend-label-click=${this._handleLegendLabelClick}
></ha-chart-base>
@@ -22,7 +22,6 @@ 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";
@@ -69,8 +68,8 @@ export class StateHistoryChartTimeline extends LitElement {
@property({ attribute: false }) public chartIndex?;
@property({ attribute: "zoom-slider" })
public zoomSlider: ZoomSliderMode = "auto";
@property({ attribute: "hide-zoom-controls", type: Boolean })
public hideZoomControls = false;
@state() private _chartData: CustomSeriesOption[] = [];
@@ -106,7 +105,7 @@ export class StateHistoryChartTimeline extends LitElement {
grow-for-zoom-slider
@chart-click=${this._handleChartClick}
@chart-zoom=${this._handleDataZoom}
.zoomSlider=${this.zoomSlider}
.hideZoomControls=${this.hideZoomControls}
></ha-chart-base>
`;
}
+2 -2
View File
@@ -218,7 +218,7 @@ export class StateHistoryCharts extends LitElement {
@chart-zoom-with-index=${this._handleTimelineSync}
.height=${this.virtualize ? undefined : this.height}
.expandLegend=${this.expandLegend}
.zoomSlider=${this.syncCharts ? "never" : "auto"}
?hide-zoom-controls=${this.syncCharts}
></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}
.zoomSlider=${this.syncCharts ? "never" : "auto"}
?hide-zoom-controls=${this.syncCharts}
></state-history-chart-timeline>
</div> `;
};
+3 -79
View File
@@ -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, query, state } from "lit/decorators";
import { customElement, property, 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,11 +49,7 @@ import type {
HomeAssistantUI,
} from "../../types";
import { getPeriodicAxisLabelConfig } from "./axis-label";
import type {
CustomLegendOption,
HaChartBase,
ZoomSliderMode,
} from "./ha-chart-base";
import type { CustomLegendOption } from "./ha-chart-base";
import "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import "./ha-chart-tooltip-marker";
@@ -132,19 +128,6 @@ 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"];
@@ -260,15 +243,9 @@ export class StatisticsChart extends LitElement {
return html`
<ha-chart-base
.data=${this._highlightedChartData(
this._chartData,
this.highlight,
getComputedStyle(this).getPropertyValue("--primary-color").trim()
)}
.data=${this._chartData}
.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}
@@ -282,59 +259,6 @@ 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");
+12 -21
View File
@@ -1,5 +1,6 @@
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";
@@ -16,7 +17,6 @@ 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 "../item/ha-list-item-button";
import "../list/ha-list-base";
import "../ha-list";
import "../ha-list-item";
import "../ha-time-input";
import type { HaTimeInput } from "../ha-time-input";
import type { DateRangePickerRanges } from "./ha-date-range-picker";
@@ -166,19 +166,11 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
}
return html`
<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 @action=${this._setDateRange} activatable>
${Object.keys(this.ranges!).map(
(name) => html`<ha-list-item>${name}</ha-list-item>`
)}
</ha-list-base>
</ha-list>
`;
}
@@ -351,12 +343,11 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
this._saveDateRangePreset(chip.range, chip.index);
}
private _setDateRange(
ev: HASSDomCurrentTargetEvent<
HTMLElement & { range: [Date, Date]; index: number }
>
) {
this._saveDateRangePreset(ev.currentTarget.range, ev.currentTarget.index);
private _setDateRange(ev: HASSDomEvent<ActionDetail>) {
const dateRange: [Date, Date] = Object.values(this.ranges!)[
ev.detail.index
];
this._saveDateRangePreset(dateRange, ev.detail.index);
}
private _saveDateRangePreset(range: [Date, Date], index: number) {
+52
View File
@@ -0,0 +1,52 @@
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;
}
}
@@ -73,14 +73,15 @@ import "../ha-dropdown";
import type { HaDropdownSelectEvent } from "../ha-dropdown";
import "../ha-dropdown-item";
import "../ha-icon-button";
import "../ha-list";
import "../ha-list-item";
import "../input/ha-input-search";
import type { HaInputSearch } from "../input/ha-input-search";
import "./ha-media-browser-thumbnail";
import "../ha-spinner";
import "../ha-svg-icon";
import "../ha-tooltip";
import "../item/ha-list-item-base";
import "../item/ha-list-item-button";
import "../list/ha-list-base";
import "./ha-browse-media-manual";
import type { ManualMediaPickedEvent } from "./ha-browse-media-manual";
import "./ha-browse-media-tts";
@@ -508,7 +509,6 @@ export class HaMediaPlayerBrowse extends LitElement {
)
: [];
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
const childrenMediaClass = currentItem.children_media_class
? MediaClassBrowserSettings[currentItem.children_media_class]
: MediaClassBrowserSettings.directory;
@@ -736,7 +736,7 @@ export class HaMediaPlayerBrowse extends LitElement {
}
`
: html`
<ha-list>
<ha-list-base>
<lit-virtualizer
scroller
.items=${children}
@@ -749,26 +749,18 @@ export class HaMediaPlayerBrowse extends LitElement {
${
notShown
? html`
<ha-list-item
noninteractive
class="not-shown"
.graphic=${
mediaClass.show_list_images
? "medium"
: "avatar"
}
>
<span class="title">
<ha-list-item-base class="not-shown">
<span slot="headline" class="title">
${this._i18n.localize(
"ui.components.media-browser.not_shown",
{ count: notShown }
)}
</span>
</ha-list-item>
</ha-list-item-base>
`
: ""
}
</ha-list>
</ha-list-base>
`
}
</div>
@@ -1073,10 +1065,10 @@ export class HaMediaPlayerBrowse extends LitElement {
const showImage = mediaClass.show_list_images && !!child.thumbnail;
return html`
<ha-list-item
<ha-list-item-button
@click=${this._childClicked}
.item=${child}
.graphic=${mediaClass.show_list_images ? "medium" : "avatar"}
class=${classMap({ "with-image": mediaClass.show_list_images })}
>
${
!showImage && !child.can_play
@@ -1088,14 +1080,14 @@ export class HaMediaPlayerBrowse extends LitElement {
: child.media_class
].icon
}
slot="graphic"
slot="start"
></ha-svg-icon>`
: html`<div
class=${classMap({
graphic: true,
thumbnail: mediaClass.show_list_images === true,
})}
slot="graphic"
slot="start"
>
${
showImage
@@ -1122,8 +1114,8 @@ export class HaMediaPlayerBrowse extends LitElement {
}
</div>`
}
<span class="title">${child.title}</span>
</ha-list-item>
<span slot="headline" class="title">${child.title}</span>
</ha-list-item-button>
`;
};
@@ -1529,23 +1521,22 @@ export class HaMediaPlayerBrowse extends LitElement {
/* ============= CHILDREN ============= */
ha-list {
--mdc-list-vertical-padding: 0;
--mdc-list-item-graphic-margin: 0;
--mdc-theme-text-icon-on-background: var(--secondary-text-color);
ha-list-base {
margin-top: 10px;
}
ha-list li:last-child {
display: none;
ha-list-base::part(base) {
gap: 0;
padding: 0;
}
ha-list li[divider] {
border-bottom-color: var(--divider-color);
}
ha-list-item {
ha-list-item-button {
width: 100%;
--ha-row-item-gap: 0;
}
ha-list-item-button.with-image {
--ha-row-item-gap: var(--ha-space-4);
}
div.children {
@@ -1680,19 +1671,21 @@ export class HaMediaPlayerBrowse extends LitElement {
padding-left: 16px;
}
ha-list-item .graphic {
ha-list-item-button .graphic {
position: relative;
border-radius: var(--ha-border-radius-sm);
overflow: hidden;
line-height: initial;
width: 56px;
height: 56px;
}
ha-list-item .graphic ha-media-browser-thumbnail {
ha-list-item-button .graphic ha-media-browser-thumbnail {
position: absolute;
inset: 0;
}
ha-list-item .graphic .play {
ha-list-item-button .graphic .play {
position: absolute;
inset: 0;
margin: auto;
@@ -1703,20 +1696,18 @@ export class HaMediaPlayerBrowse extends LitElement {
--ha-icon-button-size: 40px;
}
ha-list-item:hover .graphic .play {
ha-list-item-button:hover .graphic .play {
opacity: 1;
color: var(--primary-text-color);
}
ha-list-item .graphic .play.show {
ha-list-item-button .graphic .play.show {
opacity: 1;
background-color: transparent;
}
ha-list-item .title {
margin-left: 16px;
margin-inline-start: 16px;
margin-inline-end: initial;
ha-list-item-button .title {
white-space: normal;
}
/* ============= Narrow ============= */
+16 -16
View File
@@ -5,10 +5,9 @@ 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 {
@@ -93,22 +92,23 @@ export class HaConfigPerson extends LitElement {
</span>
<ha-card outlined class="storage">
<ha-list-base>
<ha-list>
${this._storageItems.map(
(entry) => html`
<ha-list-item-button
<ha-list-item
graphic="avatar"
@click=${this._openEditEntry}
.entry=${entry}
>
<ha-person-badge
.person=${entry}
slot="start"
slot="graphic"
></ha-person-badge>
<span slot="headline">${entry.name}</span>
</ha-list-item-button>
<span>${entry.name}</span>
</ha-list-item>
`
)}
</ha-list-base>
</ha-list>
${
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-base>
<ha-list>
${this._configItems.map(
(entry) => html`
<ha-list-item-base>
<ha-list-item graphic="avatar">
<ha-person-badge
.person=${entry}
slot="start"
slot="graphic"
></ha-person-badge>
<span slot="headline">${entry.name}</span>
</ha-list-item-base>
<span>${entry.name}</span>
</ha-list-item>
`
)}
</ha-list-base>
</ha-list>
</ha-card>
`
: nothing
@@ -299,7 +299,7 @@ export class HaConfigPerson extends LitElement {
align-items: center;
justify-content: space-around;
}
ha-list-base:has(+ .empty) {
ha-list:has(+ .empty) {
display: none;
}
`;
@@ -73,10 +73,6 @@ 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
@@ -309,8 +305,6 @@ class ViewMountDialog extends DirtyStateProviderMixin<
private _originalType?: SupervisorMountType;
private _suggestedName?: string;
@state() private _diskIdentity?: string;
public connectedCallback(): void {
@@ -535,9 +529,7 @@ 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";
}
@@ -564,32 +556,6 @@ 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, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import {
@@ -78,6 +78,7 @@ import type {
HomeAssistantConfig,
HomeAssistantInternationalization,
} from "../../../types";
import { animationStyles } from "../../../resources/theme/animations.globals";
const RANGE_KEYS: DateRange[] = [
"today",
@@ -150,10 +151,6 @@ 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;
@@ -382,15 +379,10 @@ export class HuiEnergyPeriodSelector extends LitElement {
: nothing
}
${
this._loading || this._loadingLeaving
this._loading
? html`<ha-spinner
class=${classMap({
"loading-indicator": true,
leaving: this._loadingLeaving,
})}
class="loading-indicator"
size="small"
@transitionend=${this._loadingTransitionDone}
@transitioncancel=${this._loadingTransitionDone}
></ha-spinner>`
: nothing
}
@@ -735,10 +727,6 @@ 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;
@@ -771,30 +759,10 @@ 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,
@@ -810,6 +778,7 @@ export class HuiEnergyPeriodSelector extends LitElement {
);
static styles = css`
${animationStyles}
:host {
display: block;
}
@@ -874,15 +843,7 @@ export class HuiEnergyPeriodSelector extends LitElement {
position: absolute;
right: var(--ha-space-2);
align-items: center;
transition: opacity var(--ha-animation-duration-normal) ease-in-out;
}
@starting-style {
.loading-indicator {
opacity: 0;
}
}
.loading-indicator.leaving {
opacity: 0;
animation: fade-in var(--ha-animation-duration-normal) ease-in-out;
}
.date-actions .overflow {
display: flex;
-8
View File
@@ -15,8 +15,6 @@ import {
DataZoomComponent,
VisualMapComponent,
ToolboxComponent,
MarkAreaComponent,
BrushComponent,
} from "echarts/components";
// Features like Universal Transition and Label Layout
@@ -50,8 +48,6 @@ import type {
GridComponentOption,
DataZoomComponentOption,
VisualMapComponentOption,
MarkAreaComponentOption,
BrushComponentOption,
} from "echarts/components";
import type { ComposeOption } from "echarts/core";
@@ -66,8 +62,6 @@ export type ECOption = ComposeOption<
| GridComponentOption
| DataZoomComponentOption
| VisualMapComponentOption
| MarkAreaComponentOption
| BrushComponentOption
| SankeySeriesOption
| GraphSeriesOption
| SunburstSeriesOption
@@ -97,8 +91,6 @@ echarts.use([
UniversalTransition,
CanvasRenderer,
ToolboxComponent,
MarkAreaComponent,
BrushComponent,
]);
export { LinearGradient };
@@ -1,10 +0,0 @@
// 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";
+15 -10
View File
@@ -14,6 +14,8 @@ import {
polyfillTimeZoneData,
} from "./locale-data-polyfill";
let polyfilled = false;
const _polyfillTimeZoneData = polyfillTimeZoneData;
// The DateTimeFormat and DurationFormat polyfills call BigInt() when formatting
@@ -45,6 +47,10 @@ const setDefaultTimeZone = () => {
};
const polyfillIntl = async () => {
if (polyfilled) {
return;
}
polyfilled = true;
const locale = getLocalLanguage();
const polyfills: Promise<unknown>[] = [];
if (shouldPolyfillGetCanonicalLocales()) {
@@ -73,10 +79,14 @@ const polyfillIntl = async () => {
polyfills.push(import("@formatjs/intl-numberformat/polyfill-force"));
}
if (shouldPolyfillPluralRules(locale)) {
// 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"));
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")
)
);
}
if (shouldPolyfillRelativeTimeFormat(locale)) {
polyfills.push(import("@formatjs/intl-relativetimeformat/polyfill-force"));
@@ -90,9 +100,4 @@ const polyfillIntl = async () => {
);
};
// 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());
await polyfillIntl();
-2
View File
@@ -41,8 +41,6 @@ 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;