Compare commits

..

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
fc9a6edf82 Pass layout property to child cards in stack containers
Co-authored-by: MindFreeze <5219205+MindFreeze@users.noreply.github.com>
2025-11-10 10:00:53 +00:00
copilot-swe-agent[bot]
e38729cc5c Initial plan 2025-11-10 09:49:38 +00:00
51 changed files with 1752 additions and 1565 deletions

View File

@@ -89,8 +89,8 @@
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.9.5",
"@vaadin/vaadin-themable-mixin": "24.9.5",
"@vaadin/combo-box": "24.9.4",
"@vaadin/vaadin-themable-mixin": "24.9.4",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
@@ -178,7 +178,7 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.8",
"@vitest/coverage-v8": "4.0.7",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
@@ -219,7 +219,7 @@
"typescript": "5.9.3",
"typescript-eslint": "8.46.3",
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.8",
"vitest": "4.0.7",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"

View File

@@ -119,8 +119,8 @@ type Thresholds = Record<
>;
export const DEFAULT_THRESHOLDS: Thresholds = {
second: 59, // seconds to minute
minute: 59, // minutes to hour
second: 45, // seconds to minute
minute: 45, // minutes to hour
hour: 22, // hour to day
day: 5, // day to week
week: 4, // week to months

View File

@@ -6,8 +6,7 @@ export function downSampleLineData<
data: T[] | undefined,
maxDetails: number,
minX?: number,
maxX?: number,
useMean = false
maxX?: number
): T[] {
if (!data) {
return [];
@@ -18,13 +17,15 @@ export function downSampleLineData<
const min = minX ?? getPointData(data[0]!)[0];
const max = maxX ?? getPointData(data[data.length - 1]!)[0];
const step = Math.ceil((max - min) / Math.floor(maxDetails));
// Group points into frames
const frames = new Map<
number,
{ point: (typeof data)[number]; x: number; y: number }[]
{
min: { point: (typeof data)[number]; x: number; y: number };
max: { point: (typeof data)[number]; x: number; y: number };
}
>();
// Group points into frames
for (const point of data) {
const pointData = getPointData(point);
if (!Array.isArray(pointData)) continue;
@@ -35,53 +36,28 @@ export function downSampleLineData<
const frameIndex = Math.floor((x - min) / step);
const frame = frames.get(frameIndex);
if (!frame) {
frames.set(frameIndex, [{ point, x, y }]);
frames.set(frameIndex, { min: { point, x, y }, max: { point, x, y } });
} else {
frame.push({ point, x, y });
if (frame.min.y > y) {
frame.min = { point, x, y };
}
if (frame.max.y < y) {
frame.max = { point, x, y };
}
}
}
// Convert frames back to points
const result: T[] = [];
if (useMean) {
// Use mean values for each frame
for (const [_i, framePoints] of frames) {
const sumY = framePoints.reduce((acc, p) => acc + p.y, 0);
const meanY = sumY / framePoints.length;
const sumX = framePoints.reduce((acc, p) => acc + p.x, 0);
const meanX = sumX / framePoints.length;
const firstPoint = framePoints[0].point;
const pointData = getPointData(firstPoint);
const meanPoint = (
Array.isArray(pointData) ? [meanX, meanY] : { value: [meanX, meanY] }
) as T;
result.push(meanPoint);
for (const [_i, frame] of frames) {
// Use min/max points to preserve visual accuracy
// The order of the data must be preserved so max may be before min
if (frame.min.x > frame.max.x) {
result.push(frame.max.point);
}
} else {
// Use min/max values for each frame
for (const [_i, framePoints] of frames) {
let minPoint = framePoints[0];
let maxPoint = framePoints[0];
for (const p of framePoints) {
if (p.y < minPoint.y) {
minPoint = p;
}
if (p.y > maxPoint.y) {
maxPoint = p;
}
}
// The order of the data must be preserved so max may be before min
if (minPoint.x > maxPoint.x) {
result.push(maxPoint.point);
}
result.push(minPoint.point);
if (minPoint.x < maxPoint.x) {
result.push(maxPoint.point);
}
result.push(frame.min.point);
if (frame.min.x < frame.max.x) {
result.push(frame.max.point);
}
}

View File

@@ -427,7 +427,6 @@ export class HaChartBase extends LitElement {
...axis.axisPointer?.handle,
show: true,
},
label: { show: false },
},
}
: axis

View File

@@ -62,7 +62,6 @@ class HaDataTableLabels extends LitElement {
@click=${clickAction ? this._labelClicked : undefined}
@keydown=${clickAction ? this._labelClicked : undefined}
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label?.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`

View File

@@ -197,6 +197,9 @@ export class HaDevicePicker extends LitElement {
const placeholder =
this.placeholder ??
this.hass.localize("ui.components.device-picker.placeholder");
const notFoundLabel = this.hass.localize(
"ui.components.device-picker.no_match"
);
const valueRenderer = this._valueRenderer(this._configEntryLookup);
@@ -206,10 +209,7 @@ export class HaDevicePicker extends LitElement {
.autofocus=${this.autofocus}
.label=${this.label}
.searchLabel=${this.searchLabel}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize(
"ui.components.device-picker.no_devices"
)}
.notFoundLabel=${notFoundLabel}
.placeholder=${placeholder}
.value=${this.value}
.rowRenderer=${this._rowRenderer}
@@ -233,11 +233,6 @@ export class HaDevicePicker extends LitElement {
this.value = value;
fireEvent(this, "value-changed", { value });
}
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.device-picker.no_match", {
term: html`<b>${search}</b>`,
});
}
declare global {

View File

@@ -269,6 +269,9 @@ export class HaEntityPicker extends LitElement {
const placeholder =
this.placeholder ??
this.hass.localize("ui.components.entity.entity-picker.placeholder");
const notFoundLabel = this.hass.localize(
"ui.components.entity.entity-picker.no_match"
);
return html`
<ha-generic-picker
@@ -279,7 +282,7 @@ export class HaEntityPicker extends LitElement {
.label=${this.label}
.helper=${this.helper}
.searchLabel=${this.searchLabel}
.notFoundLabel=${this._notFoundLabel}
.notFoundLabel=${notFoundLabel}
.placeholder=${placeholder}
.value=${this.addButton ? undefined : this.value}
.rowRenderer=${this._rowRenderer}
@@ -353,11 +356,6 @@ export class HaEntityPicker extends LitElement {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.entity.entity-picker.no_match", {
term: html`<b>${search}</b>`,
});
}
declare global {

View File

@@ -271,6 +271,7 @@ export class HaStatisticPicker extends LitElement {
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
output.push({
@@ -278,6 +279,7 @@ export class HaStatisticPicker extends LitElement {
statistic_id: id,
primary,
secondary,
a11y_label: a11yLabel,
stateObj: stateObj,
type: "entity",
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
@@ -456,6 +458,9 @@ export class HaStatisticPicker extends LitElement {
const placeholder =
this.placeholder ??
this.hass.localize("ui.components.statistic-picker.placeholder");
const notFoundLabel = this.hass.localize(
"ui.components.statistic-picker.no_match"
);
return html`
<ha-generic-picker
@@ -463,10 +468,7 @@ export class HaStatisticPicker extends LitElement {
.autofocus=${this.autofocus}
.allowCustomValue=${this.allowCustomEntity}
.label=${this.label}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize(
"ui.components.statistic-picker.no_statistics"
)}
.notFoundLabel=${notFoundLabel}
.placeholder=${placeholder}
.value=${this.value}
.rowRenderer=${this._rowRenderer}
@@ -519,11 +521,6 @@ export class HaStatisticPicker extends LitElement {
await this.updateComplete;
await this._picker?.open();
}
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.statistic-picker.no_match", {
term: html`<b>${search}</b>`,
});
}
declare global {

View File

@@ -369,10 +369,9 @@ export class HaAreaPicker extends LitElement {
.autofocus=${this.autofocus}
.label=${this.label}
.helper=${this.helper}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
.disabled=${this.disabled}
.required=${this.required}
.notFoundLabel=${this.hass.localize(
"ui.components.area-picker.no_match"
)}
.placeholder=${placeholder}
.value=${this.value}
.getItems=${this._getItems}
@@ -426,11 +425,6 @@ export class HaAreaPicker extends LitElement {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.area-picker.no_match", {
term: html`<b>${search}</b>`,
});
}
declare global {

View File

@@ -109,10 +109,7 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
.selected=${(this.value || []).includes(label.label_id)}
hasMeta
>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon
slot="icon"

View File

@@ -383,9 +383,8 @@ export class HaFloorPicker extends LitElement {
.hass=${this.hass}
.autofocus=${this.autofocus}
.label=${this.label}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize(
"ui.components.floor-picker.no_floors"
.notFoundLabel=${this.hass.localize(
"ui.components.floor-picker.no_match"
)}
.placeholder=${placeholder}
.value=${this.value}
@@ -445,11 +444,6 @@ export class HaFloorPicker extends LitElement {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.floor-picker.no_match", {
term: html`<b>${search}</b>`,
});
}
declare global {

View File

@@ -25,6 +25,9 @@ import "./ha-svg-icon";
export class HaGenericPicker extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@@ -46,11 +49,8 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ attribute: false })
public getItems?: (
searchString?: string,
section?: string
) => (PickerComboBoxItem | string)[];
@property({ attribute: false, type: Array })
public getItems?: () => PickerComboBoxItem[];
@property({ attribute: false, type: Array })
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
@@ -64,11 +64,8 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: false })
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
@property({ attribute: false })
public notFoundLabel?: string | ((search: string) => string);
@property({ attribute: "empty-label" })
public emptyLabel?: string;
@property({ attribute: "not-found-label", type: String })
public notFoundLabel?: string;
@property({ attribute: "popover-placement" })
public popoverPlacement:
@@ -88,25 +85,6 @@ export class HaGenericPicker extends LitElement {
/** If set picker shows an add button instead of textbox when value isn't set */
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
/** Section filter buttons for the list, section headers needs to be defined in getItems as strings */
@property({ attribute: false }) public sections?: (
| {
id: string;
label: string;
}
| "separator"
)[];
@property({ attribute: false }) public sectionTitleFunction?: (listInfo: {
firstIndex: number;
lastIndex: number;
firstItem: PickerComboBoxItem | string;
secondItem: PickerComboBoxItem | string;
itemsCount: number;
}) => string | undefined;
@property({ attribute: "selected-section" }) public selectedSection?: string;
@query(".container") private _containerElement?: HTMLDivElement;
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
@@ -119,11 +97,6 @@ export class HaGenericPicker extends LitElement {
@state() private _openedNarrow = false;
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
private _narrow = false;
// helper to set new value after closing picker, to avoid flicker
@@ -216,19 +189,16 @@ export class HaGenericPicker extends LitElement {
<ha-picker-combo-box
.hass=${this.hass}
.allowCustomValue=${this.allowCustomValue}
.label=${this.searchLabel}
.label=${this.searchLabel ??
(this.hass?.localize("ui.common.search") || "Search")}
.value=${this.value}
@value-changed=${this._valueChanged}
.rowRenderer=${this.rowRenderer}
.notFoundLabel=${this.notFoundLabel}
.emptyLabel=${this.emptyLabel}
.getItems=${this.getItems}
.getAdditionalItems=${this.getAdditionalItems}
.searchFn=${this.searchFn}
.mode=${dialogMode ? "dialog" : "popover"}
.sections=${this.sections}
.sectionTitleFunction=${this.sectionTitleFunction}
.selectedSection=${this.selectedSection}
></ha-picker-combo-box>
`;
}

View File

@@ -224,9 +224,8 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.autofocus=${this.autofocus}
.label=${this.label}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize(
"ui.components.label-picker.no_labels"
.notFoundLabel=${this.hass.localize(
"ui.components.label-picker.no_match"
)}
.addButtonLabel=${this.hass.localize("ui.components.label-picker.add")}
.placeholder=${placeholder}
@@ -289,11 +288,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
fireEvent(this, "change");
}, 0);
}
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.label-picker.no_match", {
term: html`<b>${search}</b>`,
});
}
declare global {

View File

@@ -1,32 +1,17 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { uid } from "../common/util/uid";
import "./ha-tooltip";
@customElement("ha-label")
class HaLabel extends LitElement {
@property({ type: Boolean, reflect: true }) dense = false;
@property({ attribute: "description" })
public description?: string;
private _elementId = "label-" + uid();
protected render(): TemplateResult {
return html`
<ha-tooltip
.for=${this._elementId}
.disabled=${!this.description?.trim()}
>
${this.description}
</ha-tooltip>
<div class="container" .id=${this._elementId}>
<span class="content">
<slot name="icon"></slot>
<slot></slot>
</span>
</div>
<span class="content">
<slot name="icon"></slot>
<slot></slot>
</span>
`;
}
@@ -51,7 +36,9 @@ class HaLabel extends LitElement {
font-weight: var(--ha-font-weight-medium);
line-height: var(--ha-line-height-condensed);
letter-spacing: 0.1px;
vertical-align: middle;
height: 32px;
padding: 0 16px;
border-radius: var(--ha-border-radius-xl);
color: var(--ha-label-text-color);
--mdc-icon-size: 12px;
@@ -79,23 +66,14 @@ class HaLabel extends LitElement {
display: flex;
}
.container {
display: flex;
position: relative;
height: 100%;
padding: 0 16px;
}
span {
display: inline-flex;
}
:host([dense]) {
height: 20px;
border-radius: var(--ha-border-radius-md);
}
:host([dense]) .container {
padding: 0 12px;
border-radius: var(--ha-border-radius-md);
}
:host([dense]) ::slotted([slot="icon"]) {
margin-right: 4px;

View File

@@ -21,7 +21,6 @@ import "./chips/ha-input-chip";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-label-picker";
import type { HaLabelPicker } from "./ha-label-picker";
import "./ha-tooltip";
@customElement("ha-labels-picker")
export class HaLabelsPicker extends SubscribeMixin(LitElement) {
@@ -143,17 +142,9 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
const color = label?.color
? computeCssColor(label.color)
: undefined;
const elementId = "label-" + label.label_id;
return html`
<ha-tooltip
.for=${elementId}
.disabled=${!label?.description?.trim()}
>
${label?.description}
</ha-tooltip>
<ha-input-chip
.item=${label}
.id=${elementId}
@remove=${this._removeItem}
@click=${this._openDetail}
.disabled=${this.disabled}

View File

@@ -125,10 +125,9 @@ export class HaLanguagePicker extends LitElement {
.hass=${this.hass}
.autofocus=${this.autofocus}
popover-placement="bottom-end"
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass?.localize(
"ui.components.language-picker.no_languages"
) || "No languages available"}
.notFoundLabel=${this.hass?.localize(
"ui.components.language-picker.no_match"
)}
.placeholder=${this.label ??
(this.hass?.localize("ui.components.language-picker.language") ||
"Language")}
@@ -173,15 +172,6 @@ export class HaLanguagePicker extends LitElement {
this.value = ev.detail.value;
fireEvent(this, "value-changed", { value: this.value });
}
private _notFoundLabel = (search: string) => {
const term = html`<b>${search}</b>`;
return this.hass
? this.hass.localize("ui.components.language-picker.no_match", {
term,
})
: html`No languages found for ${term}`;
};
}
declare global {

View File

@@ -1,8 +1,7 @@
import { ListItemEl } from "@material/web/list/internal/listitem/list-item";
import { styles } from "@material/web/list/internal/listitem/list-item-styles";
import { css, html, nothing, type TemplateResult } from "lit";
import { css } from "lit";
import { customElement } from "lit/decorators";
import "./ha-ripple";
export const haMdListStyles = [
styles,
@@ -26,18 +25,6 @@ export const haMdListStyles = [
@customElement("ha-md-list-item")
export class HaMdListItem extends ListItemEl {
static override styles = haMdListStyles;
protected renderRipple(): TemplateResult | typeof nothing {
if (this.type === "text") {
return nothing;
}
return html`<ha-ripple
part="ripple"
for="item"
?disabled=${this.disabled && this.type !== "link"}
></ha-ripple>`;
}
}
declare global {

View File

@@ -1,6 +1,6 @@
import type { LitVirtualizer } from "@lit-labs/virtualizer";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiMagnify, mdiMinusBoxOutline } from "@mdi/js";
import { mdiMagnify } from "@mdi/js";
import Fuse from "fuse.js";
import { css, html, LitElement, nothing } from "lit";
import {
@@ -14,12 +14,11 @@ import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { LocalizeFunc } from "../common/translations/localize";
import { HaFuse } from "../resources/fuse";
import { haStyleScrollbar } from "../resources/styles";
import { loadVirtualizer } from "../resources/virtualizer";
import type { HomeAssistant } from "../types";
import "./chips/ha-chip-set";
import "./chips/ha-filter-chip";
import "./ha-combo-box-item";
import "./ha-icon";
import "./ha-textfield";
@@ -28,18 +27,28 @@ import type { HaTextField } from "./ha-textfield";
export interface PickerComboBoxItem {
id: string;
primary: string;
a11y_label?: string;
secondary?: string;
search_labels?: string[];
sorting_label?: string;
icon_path?: string;
icon?: string;
}
const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
// Hack to force empty label to always display empty value by default in the search field
export interface PickerComboBoxItemWithLabel extends PickerComboBoxItem {
a11y_label: string;
}
const NO_MATCHING_ITEMS_FOUND_ID = "___no_matching_items_found___";
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
item
) => html`
<ha-combo-box-item type="button" compact>
<ha-combo-box-item
.type=${item.id === NO_MATCHING_ITEMS_FOUND_ID ? "text" : "button"}
compact
>
${item.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: item.icon_path
@@ -78,11 +87,8 @@ export class HaPickerComboBox extends LitElement {
@state() private _listScrolled = false;
@property({ attribute: false })
public getItems?: (
searchString?: string,
section?: string
) => (PickerComboBoxItem | string)[];
@property({ attribute: false, type: Array })
public getItems?: () => PickerComboBoxItem[];
@property({ attribute: false, type: Array })
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
@@ -90,45 +96,21 @@ export class HaPickerComboBox extends LitElement {
@property({ attribute: false })
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
@property({ attribute: false })
public notFoundLabel?: string | ((search: string) => string);
@property({ attribute: "empty-label" })
public emptyLabel?: string;
@property({ attribute: "not-found-label", type: String })
public notFoundLabel?: string;
@property({ attribute: false })
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
@property({ reflect: true }) public mode: "popover" | "dialog" = "popover";
/** Section filter buttons for the list, section headers needs to be defined in getItems as strings */
@property({ attribute: false }) public sections?: (
| {
id: string;
label: string;
}
| "separator"
)[];
@property({ attribute: false }) public sectionTitleFunction?: (listInfo: {
firstIndex: number;
lastIndex: number;
firstItem: PickerComboBoxItem | string;
secondItem: PickerComboBoxItem | string;
itemsCount: number;
}) => string | undefined;
@property({ attribute: "selected-section" }) public selectedSection?: string;
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
@query("ha-textfield") private _searchFieldElement?: HaTextField;
@state() private _items: (PickerComboBoxItem | string)[] = [];
@state() private _items: PickerComboBoxItemWithLabel[] = [];
@state() private _sectionTitle?: string;
private _allItems: (PickerComboBoxItem | string)[] = [];
private _allItems: PickerComboBoxItemWithLabel[] = [];
private _selectedItemIndex = -1;
@@ -139,8 +121,6 @@ export class HaPickerComboBox extends LitElement {
private _removeKeyboardShortcuts?: () => void;
private _search = "";
protected firstUpdated() {
this._registerKeyboardShortcuts();
}
@@ -165,142 +145,74 @@ export class HaPickerComboBox extends LitElement {
"Search"}
@input=${this._filterChanged}
></ha-textfield>
${this._renderSectionButtons()}
${this.sections?.length
? html`
<div class="section-title-wrapper">
<div
class="section-title ${!this.selectedSection &&
this._sectionTitle
? "show"
: ""}"
>
${this._sectionTitle}
</div>
</div>
`
: nothing}
<lit-virtualizer
.keyFunction=${this._keyFunction}
@scroll=${this._onScrollList}
tabindex="0"
scroller
.items=${this._items}
.renderItem=${this._renderItem}
style="min-height: 36px;"
class=${this._listScrolled ? "scrolled" : ""}
@scroll=${this._onScrollList}
@focus=${this._focusList}
@visibilityChanged=${this._visibilityChanged}
>
</lit-virtualizer> `;
}
private _renderSectionButtons() {
if (!this.sections || this.sections.length === 0) {
return nothing;
}
private _defaultNotFoundItem = memoizeOne(
(
label: this["notFoundLabel"],
localize?: LocalizeFunc
): PickerComboBoxItemWithLabel => ({
id: NO_MATCHING_ITEMS_FOUND_ID,
primary:
label ||
(localize && localize("ui.components.combo-box.no_match")) ||
"No matching items found",
icon_path: mdiMagnify,
a11y_label:
label ||
(localize && localize("ui.components.combo-box.no_match")) ||
"No matching items found",
})
);
return html`
<ha-chip-set class="sections">
${this.sections.map((section) =>
section === "separator"
? html`<div class="separator"></div>`
: html`<ha-filter-chip
@click=${this._toggleSection}
.section-id=${section.id}
.selected=${this.selectedSection === section.id}
.label=${section.label}
>
</ha-filter-chip>`
)}
</ha-chip-set>
`;
}
private _getAdditionalItems = (searchString?: string) => {
const items = this.getAdditionalItems?.(searchString) || [];
@eventOptions({ passive: true })
private _visibilityChanged(ev) {
if (
this._virtualizerElement &&
this.sectionTitleFunction &&
this.sections?.length
) {
const firstItem = this._virtualizerElement.items[ev.first];
const secondItem = this._virtualizerElement.items[ev.first + 1];
this._sectionTitle = this.sectionTitleFunction({
firstIndex: ev.first,
lastIndex: ev.last,
firstItem: firstItem as PickerComboBoxItem | string,
secondItem: secondItem as PickerComboBoxItem | string,
itemsCount: this._virtualizerElement.items.length,
});
}
}
return items.map<PickerComboBoxItemWithLabel>((item) => ({
...item,
a11y_label: item.a11y_label || item.primary,
}));
};
private _getAdditionalItems = (searchString?: string) =>
this.getAdditionalItems?.(searchString) || [];
private _getItems = (): PickerComboBoxItemWithLabel[] => {
const items = this.getItems ? this.getItems() : [];
private _getItems = () => {
let items = [
...(this.getItems
? this.getItems(this._search, this.selectedSection)
: []),
];
if (!this.sections?.length) {
items = items.sort((entityA, entityB) =>
const sortedItems = items
.map<PickerComboBoxItemWithLabel>((item) => ({
...item,
a11y_label: item.a11y_label || item.primary,
}))
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
(entityA as PickerComboBoxItem).sorting_label!,
(entityB as PickerComboBoxItem).sorting_label!,
entityA.sorting_label!,
entityB.sorting_label!,
this.hass?.locale.language ?? navigator.language
)
);
}
if (!items.length) {
items.push(NO_ITEMS_AVAILABLE_ID);
if (!sortedItems.length) {
sortedItems.push(
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
);
}
const additionalItems = this._getAdditionalItems();
items.push(...additionalItems);
if (this.mode === "dialog") {
items.push("padding"); // padding for safe area inset
}
return items;
sortedItems.push(...additionalItems);
return sortedItems;
};
private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
if (item === "padding") {
return html`<div class="bottom-padding"></div>`;
}
if (item === NO_ITEMS_AVAILABLE_ID) {
return html`
<div class="combo-box-row">
<ha-combo-box-item type="text" compact>
<ha-svg-icon
slot="start"
.path=${this._search ? mdiMagnify : mdiMinusBoxOutline}
></ha-svg-icon>
<span slot="headline"
>${this._search
? typeof this.notFoundLabel === "function"
? this.notFoundLabel(this._search)
: this.notFoundLabel ||
this.hass?.localize("ui.components.combo-box.no_match") ||
"No matching items found"
: this.emptyLabel ||
this.hass?.localize("ui.components.combo-box.no_items") ||
"No items available"}</span
>
</ha-combo-box-item>
</div>
`;
}
if (typeof item === "string") {
return html`<div class="title">${item}</div>`;
}
private _renderItem = (item: PickerComboBoxItem, index: number) => {
const renderer = this.rowRenderer || DEFAULT_ROW_RENDERER;
return html`<div
id=${`list-item-${index}`}
@@ -309,7 +221,9 @@ export class HaPickerComboBox extends LitElement {
.index=${index}
@click=${this._valueSelected}
>
${renderer(item, index)}
${item.id === NO_MATCHING_ITEMS_FOUND_ID
? DEFAULT_ROW_RENDERER(item, index)
: renderer(item, index)}
</div>`;
};
@@ -328,6 +242,10 @@ export class HaPickerComboBox extends LitElement {
const value = (ev.currentTarget as any).value as string;
const newValue = value?.trim();
if (newValue === NO_MATCHING_ITEMS_FOUND_ID) {
return;
}
fireEvent(this, "value-changed", { value: newValue });
};
@@ -338,83 +256,51 @@ export class HaPickerComboBox extends LitElement {
private _filterChanged = (ev: Event) => {
const textfield = ev.target as HaTextField;
const searchString = textfield.value.trim();
this._search = searchString;
if (this.sections?.length) {
this._items = this._getItems();
} else {
if (!searchString) {
this._items = this._allItems;
return;
}
const index = this._fuseIndex(this._allItems as PickerComboBoxItem[]);
const fuse = new HaFuse(
this._allItems as PickerComboBoxItem[],
{
shouldSort: false,
minMatchCharLength: Math.min(searchString.length, 2),
},
index
);
const results = fuse.multiTermsSearch(searchString);
let filteredItems = [...this._allItems];
if (results) {
const items: (PickerComboBoxItem | string)[] = results.map(
(result) => result.item
);
if (!items.length) {
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
}
const additionalItems = this._getAdditionalItems();
items.push(...additionalItems);
filteredItems = items;
}
if (this.searchFn) {
filteredItems = this.searchFn(
searchString,
filteredItems as PickerComboBoxItem[],
this._allItems as PickerComboBoxItem[]
);
}
this._items = filteredItems as PickerComboBoxItem[];
if (!searchString) {
this._items = this._allItems;
return;
}
const index = this._fuseIndex(this._allItems);
const fuse = new HaFuse(
this._allItems,
{
shouldSort: false,
minMatchCharLength: Math.min(searchString.length, 2),
},
index
);
const results = fuse.multiTermsSearch(searchString);
let filteredItems = this._allItems as PickerComboBoxItem[];
if (results) {
const items = results.map((result) => result.item);
if (items.length === 0) {
items.push(
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
);
}
const additionalItems = this._getAdditionalItems(searchString);
items.push(...additionalItems);
filteredItems = items;
}
if (this.searchFn) {
filteredItems = this.searchFn(
searchString,
filteredItems,
this._allItems
);
}
this._items = filteredItems as PickerComboBoxItemWithLabel[];
this._selectedItemIndex = -1;
if (this._virtualizerElement) {
this._virtualizerElement.scrollTo(0, 0);
}
};
private _toggleSection(ev: Event) {
ev.stopPropagation();
this._resetSelectedItem();
this._sectionTitle = undefined;
const section = (ev.target as HTMLElement)["section-id"] as string;
if (!section) {
return;
}
if (this.selectedSection === section) {
this.selectedSection = undefined;
} else {
this.selectedSection = section;
}
this._items = this._getItems();
// Reset scroll position when filter changes
if (this._virtualizerElement) {
this._virtualizerElement.scrollToIndex(0);
}
}
private _registerKeyboardShortcuts() {
this._removeKeyboardShortcuts = tinykeys(this, {
ArrowUp: this._selectPreviousItem,
@@ -458,7 +344,7 @@ export class HaPickerComboBox extends LitElement {
return;
}
if (typeof items[nextIndex] === "string") {
if (items[nextIndex].id === NO_MATCHING_ITEMS_FOUND_ID) {
// Skip titles, padding and empty search
if (nextIndex === maxItems) {
return;
@@ -487,7 +373,7 @@ export class HaPickerComboBox extends LitElement {
return;
}
if (typeof items[nextIndex] === "string") {
if (items[nextIndex]?.id === NO_MATCHING_ITEMS_FOUND_ID) {
// Skip titles, padding and empty search
if (nextIndex === 0) {
return;
@@ -509,6 +395,13 @@ export class HaPickerComboBox extends LitElement {
const nextIndex = 0;
if (
(this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
NO_MATCHING_ITEMS_FOUND_ID
) {
return;
}
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
this._selectedItemIndex = nextIndex + 1;
} else {
@@ -526,6 +419,13 @@ export class HaPickerComboBox extends LitElement {
const nextIndex = this._virtualizerElement.items.length - 1;
if (
(this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
NO_MATCHING_ITEMS_FOUND_ID
) {
return;
}
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
this._selectedItemIndex = nextIndex - 1;
} else {
@@ -553,7 +453,10 @@ export class HaPickerComboBox extends LitElement {
ev.stopPropagation();
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
if (this._virtualizerElement?.items.length === 1) {
if (
this._virtualizerElement?.items.length === 1 &&
firstItem.id !== NO_MATCHING_ITEMS_FOUND_ID
) {
fireEvent(this, "value-changed", {
value: firstItem.id,
});
@@ -569,7 +472,7 @@ export class HaPickerComboBox extends LitElement {
const item = this._virtualizerElement?.items[
this._selectedItemIndex
] as PickerComboBoxItem;
if (item) {
if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) {
fireEvent(this, "value-changed", { value: item.id });
}
};
@@ -581,9 +484,6 @@ export class HaPickerComboBox extends LitElement {
this._selectedItemIndex = -1;
}
private _keyFunction = (item: PickerComboBoxItem | string) =>
typeof item === "string" ? item : item.id;
static styles = [
haStyleScrollbar,
css`
@@ -658,80 +558,6 @@ export class HaPickerComboBox extends LitElement {
background-color: var(--ha-color-fill-neutral-normal-hover);
}
}
.sections {
display: flex;
flex-wrap: nowrap;
gap: var(--ha-space-2);
padding: var(--ha-space-3) var(--ha-space-3);
overflow: auto;
}
:host([mode="dialog"]) .sections {
padding: var(--ha-space-3) var(--ha-space-4);
}
.sections ha-filter-chip {
flex-shrink: 0;
--md-filter-chip-selected-container-color: var(
--ha-color-fill-primary-normal-hover
);
color: var(--primary-color);
}
.sections .separator {
height: var(--ha-space-8);
width: 0;
border: 1px solid var(--ha-color-border-neutral-quiet);
}
.section-title,
.title {
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
display: flex;
align-items: center;
}
.title {
width: 100%;
}
:host([mode="dialog"]) .title {
padding: var(--ha-space-1) var(--ha-space-4);
}
:host([mode="dialog"]) ha-textfield {
padding: 0 var(--ha-space-4);
}
.section-title-wrapper {
height: 0;
position: relative;
}
.section-title {
opacity: 0;
position: absolute;
top: 1px;
width: calc(100% - var(--ha-space-8));
}
.section-title.show {
opacity: 1;
z-index: 1;
}
.empty-search {
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
padding: var(--ha-space-3);
}
`,
];
}

View File

@@ -80,7 +80,7 @@ export const PANEL_ICONS = {
const panelSorter = (
reverseSort: string[],
defaultPanel: string | null,
defaultPanel: string,
a: PanelInfo,
b: PanelInfo,
language: string
@@ -97,7 +97,7 @@ const panelSorter = (
};
const defaultPanelSorter = (
defaultPanel: string | null,
defaultPanel: string,
a: PanelInfo,
b: PanelInfo,
language: string
@@ -158,8 +158,7 @@ export const computePanels = memoizeOne(
if (
hiddenPanels.includes(panel.url_path) ||
(!panel.title && panel.url_path !== defaultPanel) ||
(panel.default_visible === false &&
!panelsOrder.includes(panel.url_path))
(!panel.default_visible && !panelsOrder.includes(panel.url_path))
) {
return;
}

View File

@@ -1,31 +1,15 @@
import "@home-assistant/webawesome/dist/components/popover/popover";
import { consume } from "@lit/context";
// @ts-ignore
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import { mdiPlus, mdiTextureBox } from "@mdi/js";
import Fuse from "fuse.js";
import { mdiPlaylistPlus } from "@mdi/js";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing, unsafeCSS } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { customElement, property, query, state } from "lit/decorators";
import { ensureArray } from "../common/array/ensure-array";
import { fireEvent } from "../common/dom/fire_event";
import { isValidEntityId } from "../common/entity/valid_entity_id";
import { computeRTL } from "../common/util/compute_rtl";
import {
getAreasAndFloors,
type AreaFloorValue,
type FloorComboBoxItem,
} from "../data/area_floor";
import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
import { labelsContext } from "../data/context";
import { getDevices, type DevicePickerItem } from "../data/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../data/entity";
import { getEntities, type EntityComboBoxItem } from "../data/entity_registry";
import { domainToName } from "../data/integration";
import { getLabels, type LabelRegistryEntry } from "../data/label_registry";
import {
areaMeetsFilter,
deviceMeetsFilter,
@@ -34,23 +18,18 @@ import {
type TargetTypeFloorless,
} from "../data/target";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { isHelperDomain } from "../panels/config/helpers/const";
import { showHelperDetailDialog } from "../panels/config/helpers/show-dialog-helper-detail";
import { HaFuse } from "../resources/fuse";
import type { HomeAssistant } from "../types";
import { brandsUrl } from "../util/brands-url";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
import "./ha-bottom-sheet";
import "./ha-button";
import "./ha-input-helper-text";
import "./ha-svg-icon";
import "./ha-tree-indicator";
import "./target-picker/ha-target-picker-item-group";
import "./target-picker/ha-target-picker-selector";
import type { HaTargetPickerSelector } from "./target-picker/ha-target-picker-selector";
import "./target-picker/ha-target-picker-value-chip";
const EMPTY_SEARCH = "___EMPTY_SEARCH___";
const SEPARATOR = "________";
const CREATE_ID = "___create-new-entity___";
@customElement("ha-target-picker")
export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -89,54 +68,23 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property({ attribute: "add-on-top", type: Boolean }) public addOnTop = false;
@state() private _selectedSection?: TargetTypeFloorless;
@state() private _open = false;
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
@state() private _addTargetWidth = 0;
@state()
@consume({ context: labelsContext, subscribe: true })
private _labelRegistry!: LabelRegistryEntry[];
@state() private _narrow = false;
@state() private _pickerFilter?: TargetTypeFloorless;
@state() private _pickerWrapperOpen = false;
@query(".add-target-wrapper") private _addTargetWrapper?: HTMLDivElement;
@query("ha-target-picker-selector")
private _targetPickerSelectorElement?: HaTargetPickerSelector;
private _newTarget?: { type: TargetType; id: string };
private _getDevicesMemoized = memoizeOne(getDevices);
private _getLabelsMemoized = memoizeOne(getLabels);
private _getEntitiesMemoized = memoizeOne(getEntities);
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
private get _showEntityId() {
return this.hass.userData?.showEntityIdPicker;
}
private _fuseIndexes = {
area: memoizeOne((states: FloorComboBoxItem[]) =>
this._createFuseIndex(states)
),
entity: memoizeOne((states: EntityComboBoxItem[]) =>
this._createFuseIndex(states)
),
device: memoizeOne((states: DevicePickerItem[]) =>
this._createFuseIndex(states)
),
label: memoizeOne((states: PickerComboBoxItem[]) =>
this._createFuseIndex(states)
),
};
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._loadConfigEntries();
}
}
private _createFuseIndex = (states) =>
Fuse.createIndex(["search_labels"], states);
protected render() {
if (this.addOnTop) {
return html` ${this._renderPicker()} ${this._renderItems()} `;
@@ -341,63 +289,137 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
}
private _renderPicker() {
const sections = [
{
id: "entity",
label: this.hass.localize("ui.components.target-picker.type.entities"),
},
{
id: "device",
label: this.hass.localize("ui.components.target-picker.type.devices"),
},
{
id: "area",
label: this.hass.localize("ui.components.target-picker.type.areas"),
},
"separator" as const,
{
id: "label",
label: this.hass.localize("ui.components.target-picker.type.labels"),
},
];
return html`
<div class="add-target-wrapper">
<ha-generic-picker
.hass=${this.hass}
.disabled=${this.disabled}
.autofocus=${this.autofocus}
.helper=${this.helper}
.sections=${sections}
.notFoundLabel=${this._noTargetFoundLabel}
.emptyLabel=${this.hass.localize(
"ui.components.target-picker.no_targets"
)}
.sectionTitleFunction=${this._sectionTitleFunction}
.selectedSection=${this._selectedSection}
.rowRenderer=${this._renderRow}
.getItems=${this._getItems}
@value-changed=${this._targetPicked}
.addButtonLabel=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
.getAdditionalItems=${this._getAdditionalItems}
<ha-button
id="add-target-button"
size="small"
appearance="filled"
@click=${this._showPicker}
>
</ha-generic-picker>
<ha-svg-icon .path=${mdiPlaylistPlus} slot="start"></ha-svg-icon>
${this.hass.localize("ui.components.target-picker.add_target")}
</ha-button>
${!this._narrow && (this._pickerWrapperOpen || this._open)
? html`
<wa-popover
.open=${this._pickerWrapperOpen}
style="--body-width: ${this._addTargetWidth}px;"
without-arrow
distance="-4"
placement="bottom-start"
for="add-target-button"
auto-size="vertical"
auto-size-padding="16"
@wa-after-show=${this._showSelector}
@wa-after-hide=${this._hidePicker}
trap-focus
role="dialog"
aria-modal="true"
aria-label=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
>
${this._renderTargetSelector()}
</wa-popover>
`
: this._pickerWrapperOpen || this._open
? html`<ha-bottom-sheet
flexcontent
.open=${this._pickerWrapperOpen}
@wa-after-show=${this._showSelector}
@closed=${this._hidePicker}
role="dialog"
aria-modal="true"
aria-label=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
>
${this._renderTargetSelector(true)}
</ha-bottom-sheet>`
: nothing}
</div>
${this.helper
? html`<ha-input-helper-text .disabled=${this.disabled}
>${this.helper}</ha-input-helper-text
>`
: nothing}
`;
}
private _targetPicked(ev: CustomEvent<{ value: string }>) {
ev.stopPropagation();
const value = ev.detail.value;
if (value.startsWith(CREATE_ID)) {
this._createNewDomainElement(value.substring(CREATE_ID.length));
return;
}
connectedCallback() {
super.connectedCallback();
this._handleResize();
window.addEventListener("resize", this._handleResize);
}
const [type, id] = ev.detail.value.split(SEPARATOR);
this._addTarget(id, type as TargetType);
public disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("resize", this._handleResize);
}
private _handleResize = () => {
this._narrow =
window.matchMedia("(max-width: 870px)").matches ||
window.matchMedia("(max-height: 500px)").matches;
};
private _showPicker() {
this._addTargetWidth = this._addTargetWrapper?.offsetWidth || 0;
this._pickerWrapperOpen = true;
}
// wait for drawer animation to finish
private _showSelector = () => {
this._open = true;
requestAnimationFrame(() => {
this._targetPickerSelectorElement?.focus();
});
};
private _handleUpdatePickerFilter(
ev: CustomEvent<TargetTypeFloorless | undefined>
) {
this._updatePickerFilter(
typeof ev.detail === "string" ? ev.detail : undefined
);
}
private _updatePickerFilter = (filter?: TargetTypeFloorless) => {
this._pickerFilter = filter;
};
private _hidePicker(ev) {
ev.stopPropagation();
this._open = false;
this._pickerWrapperOpen = false;
if (this._newTarget) {
this._addTarget(this._newTarget.id, this._newTarget.type);
this._newTarget = undefined;
}
}
private _renderTargetSelector(dialogMode = false) {
if (!this._open) {
return nothing;
}
return html`
<ha-target-picker-selector
.hass=${this.hass}
@filter-type-changed=${this._handleUpdatePickerFilter}
.filterType=${this._pickerFilter}
@target-picked=${this._handleTargetPicked}
@create-domain-picked=${this._handleCreateDomain}
.targetValue=${this.value}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.createDomains=${this.createDomains}
.mode=${dialogMode ? "dialog" : "popover"}
></ha-target-picker-selector>
`;
}
private _addTarget(id: string, type: TargetType) {
@@ -432,7 +454,26 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
?.removeAttribute("collapsed");
}
private _createNewDomainElement = (domain: string) => {
private _handleTargetPicked = async (
ev: CustomEvent<{ type: TargetType; id: string }>
) => {
ev.stopPropagation();
this._pickerWrapperOpen = false;
if (!ev.detail.type || !ev.detail.id) {
return;
}
// save new target temporarily to add it after dialog closes
this._newTarget = ev.detail;
};
private _handleCreateDomain = (ev: CustomEvent<string>) => {
this._pickerWrapperOpen = false;
const domain = ev.detail;
showHelperDetailDialog(this, {
domain,
dialogClosedCallback: (item) => {
@@ -634,465 +675,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return undefined;
}
private _getRowType = (
item:
| PickerComboBoxItem
| (FloorComboBoxItem & { last?: boolean | undefined })
| EntityComboBoxItem
| DevicePickerItem
) => {
if (
(item as FloorComboBoxItem).type === "area" ||
(item as FloorComboBoxItem).type === "floor"
) {
return (item as FloorComboBoxItem).type;
}
if ("domain" in item) {
return "device";
}
if ("stateObj" in item) {
return "entity";
}
if (item.id === EMPTY_SEARCH) {
return "empty";
}
return "label";
};
private _sectionTitleFunction = ({
firstIndex,
lastIndex,
firstItem,
secondItem,
itemsCount,
}: {
firstIndex: number;
lastIndex: number;
firstItem: PickerComboBoxItem | string;
secondItem: PickerComboBoxItem | string;
itemsCount: number;
}) => {
if (
firstItem === undefined ||
secondItem === undefined ||
typeof firstItem === "string" ||
(typeof secondItem === "string" && secondItem !== "padding") ||
(firstIndex === 0 && lastIndex === itemsCount - 1)
) {
return undefined;
}
const type = this._getRowType(firstItem as PickerComboBoxItem);
const translationType:
| "areas"
| "entities"
| "devices"
| "labels"
| undefined =
type === "area" || type === "floor"
? "areas"
: type === "entity"
? "entities"
: type && type !== "empty"
? `${type}s`
: undefined;
return translationType
? this.hass.localize(
`ui.components.target-picker.type.${translationType}`
)
: undefined;
};
private _getItems = (searchString: string, section: string) => {
this._selectedSection = section as TargetTypeFloorless | undefined;
return this._getItemsMemoized(
this.entityFilter,
this.deviceFilter,
this.includeDomains,
this.includeDeviceClasses,
this.value,
searchString,
this._configEntryLookup,
this._selectedSection
);
};
private _getItemsMemoized = memoizeOne(
(
entityFilter: this["entityFilter"],
deviceFilter: this["deviceFilter"],
includeDomains: this["includeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
targetValue: this["value"],
searchTerm: string,
configEntryLookup: Record<string, ConfigEntry>,
filterType?: TargetTypeFloorless
) => {
const items: (
| string
| FloorComboBoxItem
| EntityComboBoxItem
| PickerComboBoxItem
)[] = [];
if (!filterType || filterType === "entity") {
let entities = this._getEntitiesMemoized(
this.hass,
includeDomains,
undefined,
entityFilter,
includeDeviceClasses,
undefined,
undefined,
targetValue?.entity_id
? ensureArray(targetValue.entity_id)
: undefined,
undefined,
`entity${SEPARATOR}`
);
if (searchTerm) {
entities = this._filterGroup(
"entity",
entities,
searchTerm,
(item: EntityComboBoxItem) =>
item.stateObj?.entity_id === searchTerm
) as EntityComboBoxItem[];
}
if (!filterType && entities.length) {
// show group title
items.push(
this.hass.localize("ui.components.target-picker.type.entities")
);
}
items.push(...entities);
}
if (!filterType || filterType === "device") {
let devices = this._getDevicesMemoized(
this.hass,
configEntryLookup,
includeDomains,
undefined,
includeDeviceClasses,
deviceFilter,
entityFilter,
targetValue?.device_id
? ensureArray(targetValue.device_id)
: undefined,
undefined,
`device${SEPARATOR}`
);
if (searchTerm) {
devices = this._filterGroup("device", devices, searchTerm);
}
if (!filterType && devices.length) {
// show group title
items.push(
this.hass.localize("ui.components.target-picker.type.devices")
);
}
items.push(...devices);
}
if (!filterType || filterType === "area") {
let areasAndFloors = this._getAreasAndFloorsMemoized(
this.hass.states,
this.hass.floors,
this.hass.areas,
this.hass.devices,
this.hass.entities,
memoizeOne((value: AreaFloorValue): string =>
[value.type, value.id].join(SEPARATOR)
),
includeDomains,
undefined,
includeDeviceClasses,
deviceFilter,
entityFilter,
targetValue?.area_id ? ensureArray(targetValue.area_id) : undefined,
targetValue?.floor_id ? ensureArray(targetValue.floor_id) : undefined
);
if (searchTerm) {
areasAndFloors = this._filterGroup(
"area",
areasAndFloors,
searchTerm
) as FloorComboBoxItem[];
}
if (!filterType && areasAndFloors.length) {
// show group title
items.push(
this.hass.localize("ui.components.target-picker.type.areas")
);
}
items.push(
...areasAndFloors.map((item, index) => {
const nextItem = areasAndFloors[index + 1];
if (
!nextItem ||
(item.type === "area" && nextItem.type === "floor")
) {
return {
...item,
last: true,
};
}
return item;
})
);
}
if (!filterType || filterType === "label") {
let labels = this._getLabelsMemoized(
this.hass,
this._labelRegistry,
includeDomains,
undefined,
includeDeviceClasses,
deviceFilter,
entityFilter,
targetValue?.label_id ? ensureArray(targetValue.label_id) : undefined,
`label${SEPARATOR}`
);
if (searchTerm) {
labels = this._filterGroup("label", labels, searchTerm);
}
if (!filterType && labels.length) {
// show group title
items.push(
this.hass.localize("ui.components.target-picker.type.labels")
);
}
items.push(...labels);
}
return items;
}
);
private _filterGroup(
type: TargetType,
items: (FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem)[],
searchTerm: string,
checkExact?: (
item: FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem
) => boolean
) {
const fuseIndex = this._fuseIndexes[type](items);
const fuse = new HaFuse(
items,
{
shouldSort: false,
minMatchCharLength: Math.min(searchTerm.length, 2),
},
fuseIndex
);
const results = fuse.multiTermsSearch(searchTerm);
let filteredItems = items;
if (results) {
filteredItems = results.map((result) => result.item);
}
if (!checkExact) {
return filteredItems;
}
// If there is exact match for entity id, put it first
const index = filteredItems.findIndex((item) => checkExact(item));
if (index === -1) {
return filteredItems;
}
const [exactMatch] = filteredItems.splice(index, 1);
filteredItems.unshift(exactMatch);
return filteredItems;
}
private _getAdditionalItems = () => this._getCreateItems(this.createDomains);
private _getCreateItems = memoizeOne(
(createDomains: this["createDomains"]) => {
if (!createDomains?.length) {
return [];
}
return createDomains.map((domain) => {
const primary = this.hass.localize(
"ui.components.entity.entity-picker.create_helper",
{
domain: isHelperDomain(domain)
? this.hass.localize(`ui.panel.config.helpers.types.${domain}`)
: domainToName(this.hass.localize, domain),
}
);
return {
id: CREATE_ID + domain,
primary: primary,
secondary: this.hass.localize(
"ui.components.entity.entity-picker.new_entity"
),
icon_path: mdiPlus,
} satisfies EntityComboBoxItem;
});
}
);
private async _loadConfigEntries() {
const configEntries = await getConfigEntries(this.hass);
this._configEntryLookup = Object.fromEntries(
configEntries.map((entry) => [entry.entry_id, entry])
);
}
private _renderRow = (
item:
| PickerComboBoxItem
| (FloorComboBoxItem & { last?: boolean | undefined })
| EntityComboBoxItem
| DevicePickerItem,
index: number
) => {
if (!item) {
return nothing;
}
const type = this._getRowType(item);
let hasFloor = false;
let rtl = false;
let showEntityId = false;
if (type === "area" || type === "floor") {
item.id = item[type]?.[`${type}_id`];
rtl = computeRTL(this.hass);
hasFloor =
type === "area" && !!(item as FloorComboBoxItem).area?.floor_id;
}
if (type === "entity") {
showEntityId = !!this._showEntityId;
}
return html`
<ha-combo-box-item
id=${`list-item-${index}`}
tabindex="-1"
.type=${type === "empty" ? "text" : "button"}
class=${type === "empty" ? "empty" : ""}
style=${(item as FloorComboBoxItem).type === "area" && hasFloor
? "--md-list-item-leading-space: var(--ha-space-12);"
: ""}
>
${(item as FloorComboBoxItem).type === "area" && hasFloor
? html`
<ha-tree-indicator
style=${styleMap({
width: "var(--ha-space-12)",
position: "absolute",
top: "var(--ha-space-0)",
left: rtl ? undefined : "var(--ha-space-1)",
right: rtl ? "var(--ha-space-1)" : undefined,
transform: rtl ? "scaleX(-1)" : "",
})}
.end=${(
item as FloorComboBoxItem & { last?: boolean | undefined }
).last}
slot="start"
></ha-tree-indicator>
`
: nothing}
${item.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: item.icon_path
? html`<ha-svg-icon
slot="start"
.path=${item.icon_path}
></ha-svg-icon>`
: type === "entity" && (item as EntityComboBoxItem).stateObj
? html`
<state-badge
slot="start"
.stateObj=${(item as EntityComboBoxItem).stateObj}
.hass=${this.hass}
></state-badge>
`
: type === "device" && (item as DevicePickerItem).domain
? html`
<img
slot="start"
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl({
domain: (item as DevicePickerItem).domain!,
type: "icon",
darkOptimized: this.hass.themes.darkMode,
})}
/>
`
: type === "floor"
? html`<ha-floor-icon
slot="start"
.floor=${(item as FloorComboBoxItem).floor!}
></ha-floor-icon>`
: type === "area"
? html`<ha-svg-icon
slot="start"
.path=${item.icon_path || mdiTextureBox}
></ha-svg-icon>`
: nothing}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${(item as EntityComboBoxItem).stateObj && showEntityId
? html`
<span slot="supporting-text" class="code">
${(item as EntityComboBoxItem).stateObj?.entity_id}
</span>
`
: nothing}
${(item as EntityComboBoxItem).domain_name &&
(type !== "entity" || !showEntityId)
? html`
<div slot="trailing-supporting-text" class="domain">
${(item as EntityComboBoxItem).domain_name}
</div>
`
: nothing}
</ha-combo-box-item>
`;
};
private _noTargetFoundLabel = (search: string) =>
this.hass.localize("ui.components.target-picker.no_target_found", {
term: html`<b>${search}</b>`,
});
static get styles(): CSSResultGroup {
return css`
.add-target-wrapper {
@@ -1101,8 +683,31 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
margin-top: var(--ha-space-3);
}
ha-generic-picker {
width: 100%;
wa-popover {
--wa-space-l: var(--ha-space-0);
}
wa-popover::part(body) {
width: min(max(var(--body-width), 336px), 600px);
max-width: min(max(var(--body-width), 336px), 600px);
max-height: 500px;
height: 70vh;
overflow: hidden;
}
@media (max-height: 1000px) {
wa-popover::part(body) {
max-height: 400px;
}
}
ha-bottom-sheet {
--ha-bottom-sheet-height: 90vh;
--ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
--ha-bottom-sheet-max-height: var(--ha-bottom-sheet-height);
--ha-bottom-sheet-max-width: 600px;
--ha-bottom-sheet-padding: var(--ha-space-0);
--ha-bottom-sheet-surface-background: var(--card-background-color);
}
${unsafeCSS(chipStyles)}

View File

@@ -545,7 +545,7 @@ export class HaTargetPickerItemRow extends LitElement {
name: entityName || deviceName || item,
context,
stateObject,
notFound: !stateObject && item !== "all" && item !== "none",
notFound: !stateObject && item !== "all",
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -128,7 +128,9 @@ class HaUserPicker extends LitElement {
.hass=${this.hass}
.autofocus=${this.autofocus}
.label=${this.label}
.notFoundLabel=${this._notFoundLabel}
.notFoundLabel=${this.hass.localize(
"ui.components.user-picker.no_match"
)}
.placeholder=${placeholder}
.value=${this.value}
.getItems=${this._getItems}
@@ -147,11 +149,6 @@ class HaUserPicker extends LitElement {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.user-picker.no_match", {
term: html`<b>${search}</b>`,
});
}
declare global {

View File

@@ -186,8 +186,7 @@ export const getDevices = (
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeDevices?: string[],
value?: string,
idPrefix = ""
value?: string
): DevicePickerItem[] => {
const devices = Object.values(hass.devices);
const entities = Object.values(hass.entities);
@@ -299,7 +298,7 @@ export const getDevices = (
const domainName = domain ? domainToName(hass.localize, domain) : undefined;
return {
id: `${idPrefix}${device.id}`,
id: device.id,
label: "",
primary:
deviceName ||

View File

@@ -344,8 +344,7 @@ export const getEntities = (
includeUnitOfMeasurement?: string[],
includeEntities?: string[],
excludeEntities?: string[],
value?: string,
idPrefix = ""
value?: string
): EntityComboBoxItem[] => {
let items: EntityComboBoxItem[] = [];
@@ -396,9 +395,10 @@ export const getEntities = (
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
return {
id: `${idPrefix}${entityId}`,
id: entityId,
primary: primary,
secondary: secondary,
domain_name: domainName,
@@ -411,6 +411,7 @@ export const getEntities = (
friendlyName,
entityId,
].filter(Boolean) as string[],
a11y_label: a11yLabel,
stateObj: stateObj,
};
});

View File

@@ -14,17 +14,11 @@ declare global {
interface FrontendUserData {
core: CoreFrontendUserData;
sidebar: SidebarFrontendUserData;
default_panel: string | null;
}
interface FrontendSystemData {
default_panel: string | null;
}
}
export type ValidUserDataKey = keyof FrontendUserData;
export type ValidSystemDataKey = keyof FrontendSystemData;
export const fetchFrontendUserData = async <
UserDataKey extends ValidUserDataKey,
>(
@@ -65,46 +59,3 @@ export const subscribeFrontendUserData = <UserDataKey extends ValidUserDataKey>(
key: userDataKey,
}
);
export const fetchFrontendSystemData = async <
SystemDataKey extends ValidSystemDataKey,
>(
conn: Connection,
key: SystemDataKey
): Promise<FrontendSystemData[SystemDataKey] | null> => {
const result = await conn.sendMessagePromise<{
value: FrontendSystemData[SystemDataKey] | null;
}>({
type: "frontend/get_system_data",
key,
});
return result.value;
};
export const saveFrontendSystemData = async <
SystemDataKey extends ValidSystemDataKey,
>(
conn: Connection,
key: SystemDataKey,
value: FrontendSystemData[SystemDataKey]
): Promise<void> =>
conn.sendMessagePromise<undefined>({
type: "frontend/set_system_data",
key,
value,
});
export const subscribeFrontendSystemData = <
SystemDataKey extends ValidSystemDataKey,
>(
conn: Connection,
systemDataKey: SystemDataKey,
onChange: (data: { value: FrontendSystemData[SystemDataKey] | null }) => void
) =>
conn.subscribeMessage<{ value: FrontendSystemData[SystemDataKey] | null }>(
onChange,
{
type: "frontend/subscribe_system_data",
key: systemDataKey,
}
);

View File

@@ -108,8 +108,7 @@ export const getLabels = (
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeLabels?: string[],
idPrefix = ""
excludeLabels?: string[]
): PickerComboBoxItem[] => {
if (!labels || labels.length === 0) {
return [];
@@ -263,7 +262,7 @@ export const getLabels = (
}
const items = outputLabels.map<PickerComboBoxItem>((label) => ({
id: `${idPrefix}${label.label_id}`,
id: label.label_id,
primary: label.name,
secondary: label.description ?? "",
icon: label.icon || undefined,

View File

@@ -4,10 +4,10 @@ import type { HomeAssistant, PanelInfo } from "../types";
/** Panel to show when no panel is picked. */
export const DEFAULT_PANEL = "lovelace";
export const getStorageDefaultPanelUrlPath = (): string | undefined => {
export const getStorageDefaultPanelUrlPath = (): string => {
const defaultPanel = window.localStorage.getItem("defaultPanel");
return defaultPanel ? JSON.parse(defaultPanel) : undefined;
return defaultPanel ? JSON.parse(defaultPanel) : DEFAULT_PANEL;
};
export const setDefaultPanel = (
@@ -17,15 +17,10 @@ export const setDefaultPanel = (
fireEvent(element, "hass-default-panel", { defaultPanel: urlPath });
};
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => {
if (!hass.defaultPanel) {
return hass.panels[DEFAULT_PANEL];
}
return hass.panels[hass.defaultPanel]
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo =>
hass.panels[hass.defaultPanel]
? hass.panels[hass.defaultPanel]
: hass.panels[DEFAULT_PANEL];
};
export const getPanelNameTranslationKey = (panel: PanelInfo) => {
if (panel.url_path === "lovelace") {

View File

@@ -105,7 +105,7 @@ class DialogEditSidebar extends LitElement {
// Add default hidden panels that are missing in hidden
for (const panel of panels) {
if (
panel.default_visible === false &&
!panel.default_visible &&
!this._order.includes(panel.url_path) &&
!this._hidden.includes(panel.url_path)
) {

View File

@@ -54,10 +54,9 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
const path = curPath();
if (["", "/"].includes(path)) {
const defaultPanel = getStorageDefaultPanelUrlPath();
if (defaultPanel) {
navigate(`/${defaultPanel}${location.search}`, { replace: true });
}
navigate(`/${getStorageDefaultPanelUrlPath()}${location.search}`, {
replace: true,
});
}
this._route = {
prefix: "",

View File

@@ -187,10 +187,6 @@ class PartialPanelResolver extends HassRouterPage {
}
private async _updateRoutes(oldPanels?: HomeAssistant["panels"]) {
if (!this.hass.defaultPanel) {
await this.hass.loadDefaultPanel();
}
this.routerOptions = this._getRoutes(this.hass.panels);
if (

View File

@@ -452,10 +452,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}

View File

@@ -1,4 +1,4 @@
import { mdiPlus, mdiTag } from "@mdi/js";
import { mdiTag, mdiPlus } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
@@ -194,9 +194,8 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.autofocus=${this.autofocus}
.label=${this.label}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize(
"ui.components.category-picker.no_categories"
.notFoundLabel=${this.hass.localize(
"ui.components.category-picker.no_match"
)}
.placeholder=${placeholder}
.value=${this.value}
@@ -255,11 +254,6 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
fireEvent(this, "change");
}, 0);
}
private _notFoundLabel = (search: string) =>
this.hass.localize("ui.components.category-picker.no_match", {
term: html`<b>${search}</b>`,
});
}
declare global {

View File

@@ -771,10 +771,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}

View File

@@ -792,10 +792,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}

View File

@@ -634,10 +634,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}

View File

@@ -45,7 +45,6 @@ import {
fetchDashboards,
updateDashboard,
} from "../../../../data/lovelace/dashboard";
import { DEFAULT_PANEL } from "../../../../data/panel";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-tabs-subpage-data-table";
@@ -287,7 +286,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
);
private _getItems = memoize(
(dashboards: LovelaceDashboard[], defaultUrlPath: string | null) => {
(dashboards: LovelaceDashboard[], defaultUrlPath: string) => {
const defaultMode = (
this.hass.panels?.lovelace?.config as LovelacePanelConfig
).mode;
@@ -403,10 +402,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
this._dashboards,
this.hass.localize
)}
.data=${this._getItems(
this._dashboards,
this.hass.defaultPanel || DEFAULT_PANEL
)}
.data=${this._getItems(this._dashboards, this.hass.defaultPanel)}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}

View File

@@ -473,10 +473,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}

View File

@@ -458,10 +458,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
.checked=${selected}
.indeterminate=${partial}
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}

View File

@@ -79,6 +79,11 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
this._errorCard.preview = this.preview;
}
}
if (changedProperties.has("layout")) {
this._cards.forEach((card) => {
card.layout = this.layout;
});
}
}
if (changedProperties.has("layout")) {
@@ -90,6 +95,7 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
const element = document.createElement("hui-card");
element.hass = this.hass;
element.preview = this.preview;
element.layout = this.layout;
element.config = cardConfig;
element.load();
return element;

View File

@@ -175,9 +175,9 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
this._names = {};
this._entities.forEach((config) => {
const stateObj = this.hass!.states[config.entity];
this._names[config.entity] =
computeLovelaceEntityName(this.hass!, stateObj, config.name) ||
config.entity;
this._names[config.entity] = stateObj
? computeLovelaceEntityName(this.hass!, stateObj, config.name)
: config.entity;
});
}

View File

@@ -50,8 +50,7 @@ export const coordinates = (
width: number,
height: number,
maxDetails: number,
limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number },
useMean = false
limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number }
) => {
history = history.filter((item) => !Number.isNaN(item[1]));
@@ -59,8 +58,7 @@ export const coordinates = (
history,
maxDetails,
limits?.minX,
limits?.maxX,
useMean
limits?.maxX
);
return calcPoints(sampledData, width, height, limits);
};
@@ -70,8 +68,7 @@ export const coordinatesMinimalResponseCompressedState = (
width: number,
height: number,
maxDetails: number,
limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number },
useMean = false
limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number }
) => {
if (!history?.length) {
return { points: [], yAxisOrigin: 0 };
@@ -83,5 +80,5 @@ export const coordinatesMinimalResponseCompressedState = (
item.lu * 1000,
Number(item.s),
]);
return coordinates(mappedHistory, width, height, maxDetails, limits, useMean);
return coordinates(mappedHistory, width, height, maxDetails, limits);
};

View File

@@ -39,31 +39,29 @@ export class HuiEntityEditor extends LitElement {
private _renderItem(item: EntityConfig, index: number) {
const stateObj = this.hass.states[item.entity];
const useDeviceName =
stateObj &&
entityUseDeviceName(stateObj, this.hass.entities, this.hass.devices);
const useDeviceName = entityUseDeviceName(
stateObj,
this.hass.entities,
this.hass.devices
);
const isRTL = computeRTL(this.hass);
const primary =
(stateObj &&
this.hass.formatEntityName(
stateObj,
useDeviceName ? { type: "device" } : { type: "entity" }
)) ||
item.entity;
const secondary =
stateObj &&
this.hass.formatEntityName(
stateObj,
useDeviceName
? [{ type: "area" }]
: [{ type: "area" }, { type: "device" }],
{
separator: isRTL ? " ◂ " : " ▸ ",
}
);
useDeviceName ? { type: "device" } : { type: "entity" }
) || item.entity;
const secondary = this.hass.formatEntityName(
stateObj,
useDeviceName
? [{ type: "area" }]
: [{ type: "area" }, { type: "device" }],
{
separator: isRTL ? " ◂ " : " ▸ ",
}
);
return html`
<ha-md-list-item class="item">

View File

@@ -4,14 +4,11 @@ import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { DOMAINS_INPUT_ROW } from "../../../common/const";
import { uid } from "../../../common/util/uid";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
import { computeDomain } from "../../../common/entity/compute_domain";
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
import "../../../components/entity/state-badge";
import "../../../components/ha-relative-time";
import "../../../components/ha-tooltip";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import type { HomeAssistant } from "../../../types";
import type { EntitiesCardEntityConfig } from "../cards/types";
@@ -39,8 +36,6 @@ export class HuiGenericEntityRow extends LitElement {
@property({ attribute: "catch-interaction", type: Boolean })
public catchInteraction?;
private _secondaryInfoElementId = "-" + uid();
protected render() {
if (!this.hass || !this.config) {
return nothing;
@@ -105,19 +100,7 @@ export class HuiGenericEntityRow extends LitElement {
? stateObj.entity_id
: this.config.secondary_info === "last-changed"
? html`
<ha-tooltip
for="last-changed${this
._secondaryInfoElementId}"
placement="right"
>
${formatDateTimeWithSeconds(
new Date(stateObj.last_changed),
this.hass.locale,
this.hass.config
)}
</ha-tooltip>
<ha-relative-time
id="last-changed${this._secondaryInfoElementId}"
.hass=${this.hass}
.datetime=${stateObj.last_changed}
capitalize
@@ -125,20 +108,7 @@ export class HuiGenericEntityRow extends LitElement {
`
: this.config.secondary_info === "last-updated"
? html`
<ha-tooltip
for="last-updated${this
._secondaryInfoElementId}"
placement="right"
>
${formatDateTimeWithSeconds(
new Date(stateObj.last_updated),
this.hass.locale,
this.hass.config
)}
</ha-tooltip>
<ha-relative-time
id="last-updated${this
._secondaryInfoElementId}"
.hass=${this.hass}
.datetime=${stateObj.last_updated}
capitalize
@@ -147,22 +117,7 @@ export class HuiGenericEntityRow extends LitElement {
: this.config.secondary_info === "last-triggered"
? stateObj.attributes.last_triggered
? html`
<ha-tooltip
for="last-triggered${this
._secondaryInfoElementId}"
placement="right"
>
${formatDateTimeWithSeconds(
new Date(
stateObj.attributes.last_triggered
),
this.hass.locale,
this.hass.config
)}
</ha-tooltip>
<ha-relative-time
id="last-triggered${this
._secondaryInfoElementId}"
.hass=${this.hass}
.datetime=${stateObj.attributes
.last_triggered}

View File

@@ -155,20 +155,16 @@ export class HuiGraphHeaderFooter
}
const width = this.clientWidth || this.offsetWidth;
// sample to 1 point per hour or 1 point per 5 pixels
const maxDetails = Math.max(
10,
const maxDetails =
this._config.detail! > 1
? Math.max(width / 5, this._config.hours_to_show!)
: this._config.hours_to_show!
);
const useMean = this._config.detail !== 2;
: this._config.hours_to_show!;
const { points } = coordinatesMinimalResponseCompressedState(
combinedHistory[this._config.entity],
width,
width / 5,
maxDetails,
{ minY: this._config.limits?.min, maxY: this._config.limits?.max },
useMean
{ minY: this._config.limits?.min, maxY: this._config.limits?.max }
);
this._coordinates = points;
},

View File

@@ -8,18 +8,13 @@ import {
subscribeServices,
} from "home-assistant-js-websocket";
import { fireEvent } from "../common/dom/fire_event";
import { computeStateName } from "../common/entity/compute_state_name";
import { promiseTimeout } from "../common/util/promise-timeout";
import { subscribeAreaRegistry } from "../data/area_registry";
import { broadcastConnectionStatus } from "../data/connection-status";
import { subscribeDeviceRegistry } from "../data/device_registry";
import {
fetchFrontendSystemData,
fetchFrontendUserData,
subscribeFrontendSystemData,
subscribeFrontendUserData,
} from "../data/frontend";
import { subscribeFrontendUserData } from "../data/frontend";
import { forwardHaptic } from "../data/haptics";
import { DEFAULT_PANEL } from "../data/panel";
import { serviceCallWillDisconnect } from "../data/service";
import {
DateFormat,
@@ -35,9 +30,10 @@ import { translationMetadata } from "../resources/translations-metadata";
import type { Constructor, HomeAssistant, ServiceCallResponse } from "../types";
import { getLocalLanguage } from "../util/common-translation";
import { fetchWithAuth } from "../util/fetch-with-auth";
import { getState, storeState } from "../util/ha-pref-storage";
import { getState } from "../util/ha-pref-storage";
import hassCallApi, { hassCallApiRaw } from "../util/hass-call-api";
import type { HassBaseEl } from "./hass-base-mixin";
import { computeStateName } from "../common/entity/compute_state_name";
export const connectionMixin = <T extends Constructor<HassBaseEl>>(
superClass: T
@@ -64,9 +60,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
services: null as any,
user: null as any,
panelUrl: (this as any)._panelUrl,
defaultPanel: null,
systemDefaultPanel: null,
userDefaultPanel: null,
defaultPanel: DEFAULT_PANEL,
language,
selectedLanguage: null,
locale: {
@@ -79,6 +73,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
},
resources: null as any,
localize: () => "",
translationMetadata,
dockedSidebar: "docked",
vibrate: true,
@@ -209,28 +204,14 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
loadFragmentTranslation: (fragment) =>
// @ts-ignore
this._loadFragmentTranslations(this.hass?.language, fragment),
loadDefaultPanel: async () => {
const [user, system] = await Promise.all([
fetchFrontendUserData(conn, "default_panel"),
fetchFrontendSystemData(conn, "default_panel"),
]);
const defaultPanel = user || system;
this._updateHass({
userDefaultPanel: user,
systemDefaultPanel: system,
defaultPanel,
});
storeState(this.hass!);
return defaultPanel;
},
formatEntityState: (stateObj, state) =>
(state != null ? state : stateObj.state) ?? "",
formatEntityAttributeName: (_stateObj, attribute) => attribute,
formatEntityAttributeValue: (stateObj, attribute, value) =>
value != null ? value : (stateObj.attributes[attribute] ?? ""),
formatEntityName: (stateObj) => computeStateName(stateObj),
...getState(),
...this._pendingHass,
formatEntityName: (stateObj) => computeStateName(stateObj),
};
this.hassConnected();
@@ -304,38 +285,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
subscribeFrontendUserData(conn, "core", ({ value: userData }) => {
this._updateHass({ userData });
});
subscribeFrontendUserData(
conn,
"default_panel",
({ value: defaultPanel }) => {
this._updateHass({
userDefaultPanel: defaultPanel,
});
// Update default panel taking into account user and system default panel
this._updateHass({
defaultPanel:
this.hass!.userDefaultPanel || this.hass!.systemDefaultPanel,
});
// Store updated default panel in local storage
storeState(this.hass!);
}
);
subscribeFrontendSystemData(
conn,
"default_panel",
({ value: defaultPanel }) => {
this._updateHass({
systemDefaultPanel: defaultPanel,
});
// Update default panel taking into account user and system default panel
this._updateHass({
defaultPanel:
this.hass!.userDefaultPanel || this.hass!.systemDefaultPanel,
});
// Store updated default panel in local storage
storeState(this.hass!);
}
);
clearInterval(this.__backendPingInterval);
this.__backendPingInterval = setInterval(() => {
if (this.hass?.connected) {

View File

@@ -1,5 +1,4 @@
import type { HASSDomEvent } from "../common/dom/fire_event";
import { saveFrontendUserData } from "../data/frontend";
import type { Constructor, HomeAssistant } from "../types";
import { storeState } from "../util/ha-pref-storage";
import type { HassBaseEl } from "./hass-base-mixin";
@@ -34,19 +33,8 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
storeState(this.hass!);
});
this.addEventListener("hass-default-panel", (ev) => {
const defaultPanel = ev.detail.defaultPanel;
this._updateHass({ defaultPanel });
this._updateHass({ defaultPanel: ev.detail.defaultPanel });
storeState(this.hass!);
try {
saveFrontendUserData(
this.hass!.connection,
"default_panel",
defaultPanel
);
} catch (err) {
// eslint-disable-next-line no-console
console.error("Failed to save default panel", err);
}
});
}
};

View File

@@ -652,7 +652,7 @@
"edit": "Edit",
"clear": "Clear",
"no_entities": "You don't have any entities",
"no_match": "No entities found for {term}",
"no_match": "No matching entities found",
"show_entities": "Show entities",
"new_entity": "Create a new entity",
"placeholder": "Select an entity",
@@ -763,7 +763,7 @@
},
"language-picker": {
"language": "Language",
"no_match": "No languages found for {term}",
"no_match": "No matching languages found",
"no_languages": "No languages available"
},
"tts-picker": {
@@ -775,7 +775,7 @@
"none": "None"
},
"user-picker": {
"no_match": "No users found for {term}",
"no_match": "No matching users found",
"user": "User",
"add_user": "Add user"
},
@@ -786,8 +786,8 @@
"clear": "Clear",
"toggle": "Toggle",
"show_devices": "Show devices",
"no_devices": "No devices available",
"no_match": "No devices found for {term}",
"no_devices": "You don't have any devices",
"no_match": "No matching devices found",
"device": "Device",
"unnamed_device": "Unnamed device",
"no_area": "No area",
@@ -801,8 +801,8 @@
"add_category": "Add category",
"add_new_sugestion": "Add new category ''{name}''",
"add_new": "Add new category…",
"no_categories": "No categories available",
"no_match": "No categories found for {term}",
"no_categories": "You don't have any categories",
"no_match": "No matching categories found",
"add_dialog": {
"title": "Add new category",
"text": "Enter the name of the new category.",
@@ -817,8 +817,8 @@
"add_new_sugestion": "Add new label ''{name}''",
"add_new": "Add new label…",
"add": "Add label",
"no_labels": "No labels available",
"no_match": "No labels found for {term}",
"no_labels": "You don't have any labels",
"no_match": "No matching labels found",
"failed_create_label": "Failed to create label."
},
"area-picker": {
@@ -827,8 +827,8 @@
"area": "Area",
"add_new_sugestion": "Add new area ''{name}''",
"add_new": "Add new area…",
"no_areas": "No areas available",
"no_match": "No areas found for {term}",
"no_areas": "You don't have any areas",
"no_match": "No matching areas found",
"unassigned_areas": "Unassigned areas",
"failed_create_area": "Failed to create area."
},
@@ -838,8 +838,8 @@
"floor": "Floor",
"add_new_sugestion": "Add new floor ''{name}''",
"add_new": "Add new floor…",
"no_floors": "No floors available",
"no_match": "No floors found for {term}",
"no_floors": "You don't have any floors",
"no_match": "No matching floors found",
"failed_create_floor": "Failed to create floor."
},
"area-filter": {
@@ -853,8 +853,8 @@
"statistic-picker": {
"statistic": "Statistic",
"placeholder": "Select a statistic",
"no_statistics": "No statistics available",
"no_match": "No statistics found for {term}",
"no_statistics": "You don't have any statistics",
"no_match": "No matching statistics found",
"no_state": "Entity without state",
"missing_entity": "Why is my entity not listed?",
"learn_more": "Learn more about statistics"
@@ -1292,8 +1292,7 @@
"add": "Add interaction"
},
"combo-box": {
"no_match": "No matching items found",
"no_items": "No items available"
"no_match": "No matching items found"
},
"suggest_with_ai": {
"label": "Suggest",

View File

@@ -248,9 +248,7 @@ export interface HomeAssistant {
vibrate: boolean;
debugConnection: boolean;
dockedSidebar: "docked" | "always_hidden" | "auto";
defaultPanel: string | null;
userDefaultPanel: string | null;
systemDefaultPanel: string | null;
defaultPanel: string;
moreInfoEntityId: string | null;
user?: CurrentUser;
userData?: CoreFrontendUserData | null;
@@ -285,7 +283,6 @@ export interface HomeAssistant {
configFlow?: Parameters<typeof getHassTranslations>[4]
): Promise<LocalizeFunc>;
loadFragmentTranslation(fragment: string): Promise<LocalizeFunc | undefined>;
loadDefaultPanel(): Promise<string | null | undefined>;
formatEntityState(stateObj: HassEntity, state?: string): string;
formatEntityAttributeValue(
stateObj: HassEntity,

View File

@@ -9,9 +9,7 @@ const STORED_STATE = [
"suspendWhenHidden",
"enableShortcuts",
"defaultPanel",
] as const;
type StoredHomeAssistant = Pick<HomeAssistant, (typeof STORED_STATE)[number]>;
];
export function storeState(hass: HomeAssistant) {
try {
@@ -33,8 +31,8 @@ export function storeState(hass: HomeAssistant) {
}
}
export function getState(): Partial<StoredHomeAssistant> {
const state = {} as Partial<StoredHomeAssistant>;
export function getState() {
const state = {};
STORED_STATE.forEach((key) => {
const storageItem = window.localStorage.getItem(key);

336
yarn.lock
View File

@@ -324,7 +324,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/parser@npm:^7.23.5, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.5":
"@babel/parser@npm:^7.23.5, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/parser@npm:7.28.5"
dependencies:
@@ -1180,7 +1180,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.4.4":
"@babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.4.4":
version: 7.28.5
resolution: "@babel/types@npm:7.28.5"
dependencies:
@@ -5082,131 +5082,131 @@ __metadata:
languageName: node
linkType: hard
"@vaadin/a11y-base@npm:~24.9.5":
version: 24.9.5
resolution: "@vaadin/a11y-base@npm:24.9.5"
"@vaadin/a11y-base@npm:~24.9.4":
version: 24.9.4
resolution: "@vaadin/a11y-base@npm:24.9.4"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/component-base": "npm:~24.9.5"
"@vaadin/component-base": "npm:~24.9.4"
lit: "npm:^3.0.0"
checksum: 10/2b77100c1525c9213d66348c789350e7e943c4fb163f478082a58a7cbc8fcdbcb49e6e3eb842ff5a4c7223b80c0210cb906466c5ceb3d0cf6eed3a83e5e30c0c
checksum: 10/911df9a219d17b8028e3e491973600b2604b7441a7c45fea9f03cc4bf69ba64c7310a31c9a43f277050e251112e38c87a224e1c91a85301276f7ce40957cb0af
languageName: node
linkType: hard
"@vaadin/combo-box@npm:24.9.5":
version: 24.9.5
resolution: "@vaadin/combo-box@npm:24.9.5"
"@vaadin/combo-box@npm:24.9.4":
version: 24.9.4
resolution: "@vaadin/combo-box@npm:24.9.4"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/a11y-base": "npm:~24.9.5"
"@vaadin/component-base": "npm:~24.9.5"
"@vaadin/field-base": "npm:~24.9.5"
"@vaadin/input-container": "npm:~24.9.5"
"@vaadin/item": "npm:~24.9.5"
"@vaadin/lit-renderer": "npm:~24.9.5"
"@vaadin/overlay": "npm:~24.9.5"
"@vaadin/vaadin-lumo-styles": "npm:~24.9.5"
"@vaadin/vaadin-material-styles": "npm:~24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.5"
"@vaadin/a11y-base": "npm:~24.9.4"
"@vaadin/component-base": "npm:~24.9.4"
"@vaadin/field-base": "npm:~24.9.4"
"@vaadin/input-container": "npm:~24.9.4"
"@vaadin/item": "npm:~24.9.4"
"@vaadin/lit-renderer": "npm:~24.9.4"
"@vaadin/overlay": "npm:~24.9.4"
"@vaadin/vaadin-lumo-styles": "npm:~24.9.4"
"@vaadin/vaadin-material-styles": "npm:~24.9.4"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.4"
lit: "npm:^3.0.0"
checksum: 10/b3b1d17fc34519458a39b6adf7bff7b109f0e4523472308d7482f53230c48ef94bf17b46155ed83a71c780fc093c8c1660c9f66676ffe95cef340fa1293b01ad
checksum: 10/3af6d052c2791774af9091877af808c1a9e15bb80c3c6278fea6e015464cbb350aed5c4913f94745be9d02110119b7354f82412cd54b56d5df90f22c48cdee75
languageName: node
linkType: hard
"@vaadin/component-base@npm:~24.9.5":
version: 24.9.5
resolution: "@vaadin/component-base@npm:24.9.5"
"@vaadin/component-base@npm:~24.9.4":
version: 24.9.4
resolution: "@vaadin/component-base@npm:24.9.4"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/vaadin-development-mode-detector": "npm:^2.0.0"
"@vaadin/vaadin-usage-statistics": "npm:^2.1.0"
lit: "npm:^3.0.0"
checksum: 10/cc68b44dd5001a139c32e3d0d6025b7285d9173c070a91ca5dfda8b4a999ed9b30a13d3638646e791d7567fbe2992eff2ccbecd41af10bd7058c212a64e06680
checksum: 10/fa8e8819de4564f5333d76402467500fd35fa399333abccd709fbe5b6d53a02e7438bc19ffda4d9119776e6c8d44abcbe5d996a0a4d415f4199fe9f3eb64dc58
languageName: node
linkType: hard
"@vaadin/field-base@npm:~24.9.5":
version: 24.9.5
resolution: "@vaadin/field-base@npm:24.9.5"
"@vaadin/field-base@npm:~24.9.4":
version: 24.9.4
resolution: "@vaadin/field-base@npm:24.9.4"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/a11y-base": "npm:~24.9.5"
"@vaadin/component-base": "npm:~24.9.5"
"@vaadin/a11y-base": "npm:~24.9.4"
"@vaadin/component-base": "npm:~24.9.4"
lit: "npm:^3.0.0"
checksum: 10/a42fde9f88a783b3899a64b238eea0022e2b02432116da03d70e92ce6280872477fc5d907f6541700fb56da6b776196ffe2958f02cea1a5bfa3e830a4f279b44
checksum: 10/0153fc5fcf912b8e5351270d026da3967300d2b8358e2ef89d354444254022ef4efa0f575d6d4bf41d7a3bc965e9afd62b8488dd2cb782c105a689230d5c7a33
languageName: node
linkType: hard
"@vaadin/icon@npm:~24.9.5":
version: 24.9.5
resolution: "@vaadin/icon@npm:24.9.5"
"@vaadin/icon@npm:~24.9.4":
version: 24.9.4
resolution: "@vaadin/icon@npm:24.9.4"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/component-base": "npm:~24.9.5"
"@vaadin/vaadin-lumo-styles": "npm:~24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.5"
"@vaadin/component-base": "npm:~24.9.4"
"@vaadin/vaadin-lumo-styles": "npm:~24.9.4"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.4"
lit: "npm:^3.0.0"
checksum: 10/cbe339af9d15b8be7b6796e0e50643a606ad53d0f21ad0571fdb257d62cfd21af9e9666f9a33864d26339749fe0c62ee8937b19d5d7a2fe0c9c48f9ac7d1e1f4
checksum: 10/ff9742abcf55b233a00b9ea3a18fcf816bec9cd49f5d2b88e2441716318fa81a9034916e55308a9db1ea789da48b86b3e25322fb8d4c74d192a6574ac42d00f8
languageName: node
linkType: hard
"@vaadin/input-container@npm:~24.9.5":
version: 24.9.5
resolution: "@vaadin/input-container@npm:24.9.5"
"@vaadin/input-container@npm:~24.9.4":
version: 24.9.4
resolution: "@vaadin/input-container@npm:24.9.4"
dependencies:
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/component-base": "npm:~24.9.5"
"@vaadin/vaadin-lumo-styles": "npm:~24.9.5"
"@vaadin/vaadin-material-styles": "npm:~24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.5"
"@vaadin/component-base": "npm:~24.9.4"
"@vaadin/vaadin-lumo-styles": "npm:~24.9.4"
"@vaadin/vaadin-material-styles": "npm:~24.9.4"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.4"
lit: "npm:^3.0.0"
checksum: 10/340abe99169b5a5c268df3b9febcc1b56867b875d22ab9af51db332a00a7187cae2eff222f65f6ff4523f7be890cd74d4fe62ca33f50520ab90b3336308833cb
checksum: 10/8b8565b8af0b01fdbcd2e89951d258e7f59690140c25606c32eba481262d47918cc792ce9cb4563b045a83ab9a314f45cc7901fca563f0098cee57b769fdb24e
languageName: node
linkType: hard
"@vaadin/item@npm:~24.9.5":
version: 24.9.5
resolution: "@vaadin/item@npm:24.9.5"
"@vaadin/item@npm:~24.9.4":
version: 24.9.4
resolution: "@vaadin/item@npm:24.9.4"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/a11y-base": "npm:~24.9.5"
"@vaadin/component-base": "npm:~24.9.5"
"@vaadin/vaadin-lumo-styles": "npm:~24.9.5"
"@vaadin/vaadin-material-styles": "npm:~24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.5"
"@vaadin/a11y-base": "npm:~24.9.4"
"@vaadin/component-base": "npm:~24.9.4"
"@vaadin/vaadin-lumo-styles": "npm:~24.9.4"
"@vaadin/vaadin-material-styles": "npm:~24.9.4"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.4"
lit: "npm:^3.0.0"
checksum: 10/4cf5251f5ea8bd24559d91b002e722fb33dd550eb944a2a00272a216e4168e501b71e2fcdcf9889b8fe1aed7c3e1c46355feb37283ceb5b02e5c9f4e026e199b
checksum: 10/9bb351e013d8066d48edea19af7ebb0718d63582c3753e2d8d49cf7837fc771d7989411b878a4ae3bd4ddd1f586622bdd7cf9a58202bb406bec6d17530c70b70
languageName: node
linkType: hard
"@vaadin/lit-renderer@npm:~24.9.5":
version: 24.9.5
resolution: "@vaadin/lit-renderer@npm:24.9.5"
"@vaadin/lit-renderer@npm:~24.9.4":
version: 24.9.4
resolution: "@vaadin/lit-renderer@npm:24.9.4"
dependencies:
lit: "npm:^3.0.0"
checksum: 10/abf3cfb4b76b1a696ca9138addc52a26519ff8d4a5dc1dc6e2f4a637cb749678188cf492f4931e623e999708d059babd73b01df973a89fe7486be4b9abc19104
checksum: 10/73b1dfe4a028232eadcf6f5ecbf676f38c94651a2147441b44d62a7954a100938c013f2e201e1d290c5b498b2e0130b8b50ad5b501d1fff653f9edce1bc6c58a
languageName: node
linkType: hard
"@vaadin/overlay@npm:~24.9.5":
version: 24.9.5
resolution: "@vaadin/overlay@npm:24.9.5"
"@vaadin/overlay@npm:~24.9.4":
version: 24.9.4
resolution: "@vaadin/overlay@npm:24.9.4"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/a11y-base": "npm:~24.9.5"
"@vaadin/component-base": "npm:~24.9.5"
"@vaadin/vaadin-lumo-styles": "npm:~24.9.5"
"@vaadin/vaadin-material-styles": "npm:~24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.5"
"@vaadin/a11y-base": "npm:~24.9.4"
"@vaadin/component-base": "npm:~24.9.4"
"@vaadin/vaadin-lumo-styles": "npm:~24.9.4"
"@vaadin/vaadin-material-styles": "npm:~24.9.4"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.4"
lit: "npm:^3.0.0"
checksum: 10/c20e67923605d94866a6d513b529a9ddf5e0baa7f208e3a3d974a040e645658b92e6501129648c20c6389d188af38ba07ece59cfb1c9a5997d10eb9b4eeebcce
checksum: 10/dedd7c1aa062e05679d0f7597f3e1e7f07d93f6a645fff83733a5e5245e18431ac94b8de6f9322c5b7239b6efb733d677c3bc09785921e267b682ee07b4f9cff
languageName: node
linkType: hard
@@ -5217,37 +5217,37 @@ __metadata:
languageName: node
linkType: hard
"@vaadin/vaadin-lumo-styles@npm:~24.9.5":
version: 24.9.5
resolution: "@vaadin/vaadin-lumo-styles@npm:24.9.5"
"@vaadin/vaadin-lumo-styles@npm:~24.9.4":
version: 24.9.4
resolution: "@vaadin/vaadin-lumo-styles@npm:24.9.4"
dependencies:
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/component-base": "npm:~24.9.5"
"@vaadin/icon": "npm:~24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.5"
checksum: 10/fd7aa0c45d71e64e4356fcb58612a1345eb5b2d5a67258826dcbad0215a3606d56340a511a4eeb6d4608c4e4237af6ad72c6cc44bb2989dd40acb1f629e9915a
"@vaadin/component-base": "npm:~24.9.4"
"@vaadin/icon": "npm:~24.9.4"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.4"
checksum: 10/e892ab2187bf4e7f6889b8d4ae93009794313e07af24921176abcdf08067123b07840d4018d75340bf2b41dd0ac7d36a71a9a999ff1e04c14cde24878dd1d5d3
languageName: node
linkType: hard
"@vaadin/vaadin-material-styles@npm:~24.9.5":
version: 24.9.5
resolution: "@vaadin/vaadin-material-styles@npm:24.9.5"
"@vaadin/vaadin-material-styles@npm:~24.9.4":
version: 24.9.4
resolution: "@vaadin/vaadin-material-styles@npm:24.9.4"
dependencies:
"@polymer/polymer": "npm:^3.0.0"
"@vaadin/component-base": "npm:~24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.5"
checksum: 10/104278b687a3acb621e0296e83aa9c8cc77358d2f80dbfe4b9a38cdd81709891b6c40f4cd5a7884021aeef5dd6553a4b5a288318c01f5d33831c338e6dbd42bd
"@vaadin/component-base": "npm:~24.9.4"
"@vaadin/vaadin-themable-mixin": "npm:~24.9.4"
checksum: 10/955b9fd2a09e3ee5ba403b6745a3a164caeea8fc76b04f57abd0cf6aa7f9d9bd24f984f95987956be5e0457b5343dc275970311937ac8a22e491b3a072cbaa00
languageName: node
linkType: hard
"@vaadin/vaadin-themable-mixin@npm:24.9.5, @vaadin/vaadin-themable-mixin@npm:~24.9.5":
version: 24.9.5
resolution: "@vaadin/vaadin-themable-mixin@npm:24.9.5"
"@vaadin/vaadin-themable-mixin@npm:24.9.4, @vaadin/vaadin-themable-mixin@npm:~24.9.4":
version: 24.9.4
resolution: "@vaadin/vaadin-themable-mixin@npm:24.9.4"
dependencies:
"@open-wc/dedupe-mixin": "npm:^1.3.0"
lit: "npm:^3.0.0"
style-observer: "npm:^0.0.8"
checksum: 10/e5f5d756abddd5c60eb4fa27d2c5ed1bbe9013850be55b12aa3c16316bd1a308ff333220be3266ad2085626f214316c53c11b7a2a0552ef913e8d500bbd4723f
checksum: 10/3cb428fd3649512a1c026f56eb9df21d24ff29d2c87813f1e8055f211241c6e93840cba46e5ac49ae459e7f9ba4a319c012255c88b71b0d4d44d6cb0d0ef9066
languageName: node
linkType: hard
@@ -5368,52 +5368,52 @@ __metadata:
languageName: node
linkType: hard
"@vitest/coverage-v8@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/coverage-v8@npm:4.0.8"
"@vitest/coverage-v8@npm:4.0.7":
version: 4.0.7
resolution: "@vitest/coverage-v8@npm:4.0.7"
dependencies:
"@bcoe/v8-coverage": "npm:^1.0.2"
"@vitest/utils": "npm:4.0.8"
ast-v8-to-istanbul: "npm:^0.3.8"
"@vitest/utils": "npm:4.0.7"
ast-v8-to-istanbul: "npm:^0.3.5"
debug: "npm:^4.4.3"
istanbul-lib-coverage: "npm:^3.2.2"
istanbul-lib-report: "npm:^3.0.1"
istanbul-lib-source-maps: "npm:^5.0.6"
istanbul-reports: "npm:^3.2.0"
magicast: "npm:^0.5.1"
std-env: "npm:^3.10.0"
magicast: "npm:^0.3.5"
std-env: "npm:^3.9.0"
tinyrainbow: "npm:^3.0.3"
peerDependencies:
"@vitest/browser": 4.0.8
vitest: 4.0.8
"@vitest/browser": 4.0.7
vitest: 4.0.7
peerDependenciesMeta:
"@vitest/browser":
optional: true
checksum: 10/f97d933c7a776eccbd293fd3f77c0dcdc683e528094ce802b2b309e282c01cca0e8ad51a03b61ec623da7034527879824df2e30f70062d7ca587db40bfa5ffd2
checksum: 10/eab89e5da9e8b3ebc0abe08419adfcd2cd54a9cd203252a8cf739017f3a3bbd87a7a6e2dd1ea50105f2edca36618c5f0a394c6f88790d9dc4523440a3588f166
languageName: node
linkType: hard
"@vitest/expect@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/expect@npm:4.0.8"
"@vitest/expect@npm:4.0.7":
version: 4.0.7
resolution: "@vitest/expect@npm:4.0.7"
dependencies:
"@standard-schema/spec": "npm:^1.0.0"
"@types/chai": "npm:^5.2.2"
"@vitest/spy": "npm:4.0.8"
"@vitest/utils": "npm:4.0.8"
chai: "npm:^6.2.0"
"@vitest/spy": "npm:4.0.7"
"@vitest/utils": "npm:4.0.7"
chai: "npm:^6.0.1"
tinyrainbow: "npm:^3.0.3"
checksum: 10/342934870fb2b11b7a47db4df2a9df2b711087fe48118568ac013386cd659b9bff8f8252bef643e9519a88d018d54e6758a733c9dedf907e9d5dc53040aa0dc3
checksum: 10/d64fa5e17b3fd1894200263c36584673e4e9f8ff055158a4fc5339a00e5132038533e8f7aa45f4f4daf0bfbedd9ccb1de2a543e11eac8c4fd507768874dbd11f
languageName: node
linkType: hard
"@vitest/mocker@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/mocker@npm:4.0.8"
"@vitest/mocker@npm:4.0.7":
version: 4.0.7
resolution: "@vitest/mocker@npm:4.0.7"
dependencies:
"@vitest/spy": "npm:4.0.8"
"@vitest/spy": "npm:4.0.7"
estree-walker: "npm:^3.0.3"
magic-string: "npm:^0.30.21"
magic-string: "npm:^0.30.19"
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0-0
@@ -5422,54 +5422,54 @@ __metadata:
optional: true
vite:
optional: true
checksum: 10/6a624e04a6fa78cc45205961bd0638486674d1cbf4589599772fdd49f58f433c821bcb1eea013ac772171fb9c81154e6f9ffcf1704f27e245860e982c3988cd4
checksum: 10/cdba9cb3808b6944b9533c9b4152c33b731b89c8204390f2e29ae5851eccb1241a12a02223d4934bf25607e967c17b89ad9fa153d939ea42c9b5171552044df7
languageName: node
linkType: hard
"@vitest/pretty-format@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/pretty-format@npm:4.0.8"
"@vitest/pretty-format@npm:4.0.7":
version: 4.0.7
resolution: "@vitest/pretty-format@npm:4.0.7"
dependencies:
tinyrainbow: "npm:^3.0.3"
checksum: 10/7e438ba6875a72b58cfe429dedc1de3025c8f87f523db24c687b2ad298e0c1a3e171e7e22ab938518a52c44383acff58d3e9936620dc45f4e97f1669e3e275da
checksum: 10/c936c0d503c665bd9565348c52280f10c990da43504fa7da027521b298bab16a6c83866d0eb91c82d7c53ba4aa299042b34a94a6545f1b7b999bf40a1d8b9c13
languageName: node
linkType: hard
"@vitest/runner@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/runner@npm:4.0.8"
"@vitest/runner@npm:4.0.7":
version: 4.0.7
resolution: "@vitest/runner@npm:4.0.7"
dependencies:
"@vitest/utils": "npm:4.0.8"
"@vitest/utils": "npm:4.0.7"
pathe: "npm:^2.0.3"
checksum: 10/cb66c1121c2701bb2400fb0969b7504aee34b400278d03f1ed19d78f8180adb88dbbd0f3f5d4ff1db49a5ae50f0e139964ff7ae32aa1d99f6e9d91d6a57c1ffe
checksum: 10/9dedaefc0c33736cfe721e1e53ecea05bb6bc9b32611bd55ca486555814aac319f0d7c6df155cebc6ece54f8c7870d810a6285c30006b49b6e511eb68a173873
languageName: node
linkType: hard
"@vitest/snapshot@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/snapshot@npm:4.0.8"
"@vitest/snapshot@npm:4.0.7":
version: 4.0.7
resolution: "@vitest/snapshot@npm:4.0.7"
dependencies:
"@vitest/pretty-format": "npm:4.0.8"
magic-string: "npm:^0.30.21"
"@vitest/pretty-format": "npm:4.0.7"
magic-string: "npm:^0.30.19"
pathe: "npm:^2.0.3"
checksum: 10/d703a4bb4979f94cc9b3c8ecaf5ce9c4741066772f1d4414db7faa1d1ed209ad5da9c85a98118e340f5f5652d680a1895efa39c060b6e3700a95806eefdf40a7
checksum: 10/df9b0c736d1a7a063eea9b9527e37acb53acaf8158469db49b1deb8b64229db30219bf0596e1981e1d7beec194085c07b06f34c466fc5b5cf114cdfa7b04de47
languageName: node
linkType: hard
"@vitest/spy@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/spy@npm:4.0.8"
checksum: 10/944223ffef7d64299d92c94ab895209b27a307ef59d2ef6f5c6c006fc1e85612c9547069b0fde7b2d93adfa484b3770b459a716f6b82f8839226132767fb661c
"@vitest/spy@npm:4.0.7":
version: 4.0.7
resolution: "@vitest/spy@npm:4.0.7"
checksum: 10/44f17971c1e8f4aaa4dcc8b26e86bcc9249a4ce8a131baac515980f3befede719494b548e2e48f871060ce2b22b8959fc85bf49db51ba4785fb6c025785b1a7b
languageName: node
linkType: hard
"@vitest/utils@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/utils@npm:4.0.8"
"@vitest/utils@npm:4.0.7":
version: 4.0.7
resolution: "@vitest/utils@npm:4.0.7"
dependencies:
"@vitest/pretty-format": "npm:4.0.8"
"@vitest/pretty-format": "npm:4.0.7"
tinyrainbow: "npm:^3.0.3"
checksum: 10/9f241a8aafbd81caec766143c8c99b8e1e76671460ff6bd1fc0921ea968c2f0d3d8305551b52509c0fb427d436ec9e2f75cafdc273d2f23c1ccaea504269a160
checksum: 10/82110c390309d3bac0ecf314f0428873db8d1df93e0a0bbc5214dca9ec820eb767666ccf2f66593d0b82bfe455ee9037727d2eb310fe24bacb3f71c45a107497
languageName: node
linkType: hard
@@ -5951,7 +5951,7 @@ __metadata:
languageName: node
linkType: hard
"ast-v8-to-istanbul@npm:^0.3.8":
"ast-v8-to-istanbul@npm:^0.3.5":
version: 0.3.8
resolution: "ast-v8-to-istanbul@npm:0.3.8"
dependencies:
@@ -6479,7 +6479,7 @@ __metadata:
languageName: node
linkType: hard
"chai@npm:^6.2.0":
"chai@npm:^6.0.1":
version: 6.2.0
resolution: "chai@npm:6.2.0"
checksum: 10/199422854e253d8711ea3f220365c6a850c450abf68b31131d2a0f703cbfc5cb48e6c81567e0adbe80e83cdcae6dba82d069a41a77c16bdf6703329c5c3447ef
@@ -9288,10 +9288,10 @@ __metadata:
"@types/tar": "npm:6.1.13"
"@types/ua-parser-js": "npm:0.7.39"
"@types/webspeechapi": "npm:0.0.29"
"@vaadin/combo-box": "npm:24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:24.9.5"
"@vaadin/combo-box": "npm:24.9.4"
"@vaadin/vaadin-themable-mixin": "npm:24.9.4"
"@vibrant/color": "npm:4.0.0"
"@vitest/coverage-v8": "npm:4.0.8"
"@vitest/coverage-v8": "npm:4.0.7"
"@vue/web-component-wrapper": "npm:1.3.0"
"@webcomponents/scoped-custom-element-registry": "npm:0.0.10"
"@webcomponents/webcomponentsjs": "npm:2.8.0"
@@ -9376,7 +9376,7 @@ __metadata:
typescript-eslint: "npm:8.46.3"
ua-parser-js: "npm:2.0.6"
vite-tsconfig-paths: "npm:5.1.4"
vitest: "npm:4.0.8"
vitest: "npm:4.0.7"
vue: "npm:2.7.16"
vue2-daterange-picker: "npm:0.6.8"
webpack-stats-plugin: "npm:1.1.3"
@@ -10922,7 +10922,7 @@ __metadata:
languageName: node
linkType: hard
"magic-string@npm:^0.30.21":
"magic-string@npm:^0.30.19":
version: 0.30.21
resolution: "magic-string@npm:0.30.21"
dependencies:
@@ -10931,14 +10931,14 @@ __metadata:
languageName: node
linkType: hard
"magicast@npm:^0.5.1":
version: 0.5.1
resolution: "magicast@npm:0.5.1"
"magicast@npm:^0.3.5":
version: 0.3.5
resolution: "magicast@npm:0.3.5"
dependencies:
"@babel/parser": "npm:^7.28.5"
"@babel/types": "npm:^7.28.5"
source-map-js: "npm:^1.2.1"
checksum: 10/ee6149994760f0b539a07f1d36631fed366ae19b9fc82e338c1cdd2a2e0b33a773635327514a6aa73faca9dc0ca37df5e5376b7b0687fb56353f431f299714c4
"@babel/parser": "npm:^7.25.4"
"@babel/types": "npm:^7.25.4"
source-map-js: "npm:^1.2.0"
checksum: 10/3a2dba6b0bdde957797361d09c7931ebdc1b30231705360eeb40ed458d28e1c3112841c3ed4e1b87ceb28f741e333c7673cd961193aa9fdb4f4946b202e6205a
languageName: node
linkType: hard
@@ -13314,7 +13314,7 @@ __metadata:
languageName: node
linkType: hard
"source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.1":
"source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1":
version: 1.2.1
resolution: "source-map-js@npm:1.2.1"
checksum: 10/ff9d8c8bf096d534a5b7707e0382ef827b4dd360a577d3f34d2b9f48e12c9d230b5747974ee7c607f0df65113732711bb701fe9ece3c7edbd43cb2294d707df3
@@ -13469,10 +13469,10 @@ __metadata:
languageName: node
linkType: hard
"std-env@npm:^3.10.0, std-env@npm:^3.7.0":
version: 3.10.0
resolution: "std-env@npm:3.10.0"
checksum: 10/19c9cda4f370b1ffae2b8b08c72167d8c3e5cfa972aaf5c6873f85d0ed2faa729407f5abb194dc33380708c00315002febb6f1e1b484736bfcf9361ad366013a
"std-env@npm:^3.7.0, std-env@npm:^3.9.0":
version: 3.9.0
resolution: "std-env@npm:3.9.0"
checksum: 10/3044b2c54a74be4f460db56725571241ab3ac89a91f39c7709519bc90fa37148784bc4cd7d3a301aa735f43bd174496f263563f76703ce3e81370466ab7c235b
languageName: node
linkType: hard
@@ -14751,24 +14751,24 @@ __metadata:
languageName: node
linkType: hard
"vitest@npm:4.0.8":
version: 4.0.8
resolution: "vitest@npm:4.0.8"
"vitest@npm:4.0.7":
version: 4.0.7
resolution: "vitest@npm:4.0.7"
dependencies:
"@vitest/expect": "npm:4.0.8"
"@vitest/mocker": "npm:4.0.8"
"@vitest/pretty-format": "npm:4.0.8"
"@vitest/runner": "npm:4.0.8"
"@vitest/snapshot": "npm:4.0.8"
"@vitest/spy": "npm:4.0.8"
"@vitest/utils": "npm:4.0.8"
"@vitest/expect": "npm:4.0.7"
"@vitest/mocker": "npm:4.0.7"
"@vitest/pretty-format": "npm:4.0.7"
"@vitest/runner": "npm:4.0.7"
"@vitest/snapshot": "npm:4.0.7"
"@vitest/spy": "npm:4.0.7"
"@vitest/utils": "npm:4.0.7"
debug: "npm:^4.4.3"
es-module-lexer: "npm:^1.7.0"
expect-type: "npm:^1.2.2"
magic-string: "npm:^0.30.21"
magic-string: "npm:^0.30.19"
pathe: "npm:^2.0.3"
picomatch: "npm:^4.0.3"
std-env: "npm:^3.10.0"
std-env: "npm:^3.9.0"
tinybench: "npm:^2.9.0"
tinyexec: "npm:^0.3.2"
tinyglobby: "npm:^0.2.15"
@@ -14779,10 +14779,10 @@ __metadata:
"@edge-runtime/vm": "*"
"@types/debug": ^4.1.12
"@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0
"@vitest/browser-playwright": 4.0.8
"@vitest/browser-preview": 4.0.8
"@vitest/browser-webdriverio": 4.0.8
"@vitest/ui": 4.0.8
"@vitest/browser-playwright": 4.0.7
"@vitest/browser-preview": 4.0.7
"@vitest/browser-webdriverio": 4.0.7
"@vitest/ui": 4.0.7
happy-dom: "*"
jsdom: "*"
peerDependenciesMeta:
@@ -14806,7 +14806,7 @@ __metadata:
optional: true
bin:
vitest: vitest.mjs
checksum: 10/b2fd9e2bb0740860f998f1ecfb948da330cc2e8dc15376669ef742420ad77fedea491731dfaefff17379457d21f06740afaa725952cbcbdcd499a532b95e5717
checksum: 10/23f872860f2f8ef7aa4a44830ff52fb385ee7879bd6952a116013cada7cc6bad7a2b72d9034d0bbf0134028b662bd00e8827021e5ff4ef6e232e8108e4f4851d
languageName: node
linkType: hard