Compare commits

...

11 Commits

Author SHA1 Message Date
Simon Lamon
db5f823b6b Remove twine and introduce trusted publishing (#27110)
* Remove twine and introduce trusted publishing

* Update release.yaml
2026-01-15 22:50:52 -05:00
uptimeZERO_
1d241aa49a Truncate long menu item labels in the sidebar (#29005) 2026-01-15 15:41:48 +00:00
Pegasus
fece231faf fix: restrict to exact match for data table (#28853) 2026-01-15 15:50:15 +01:00
Aidan Timson
fffb3c3a28 Migrate category dialogs to ha-wa-dialog (#29009) 2026-01-15 15:32:43 +01:00
renovate[bot]
fe14d436ff Update vitest monorepo to v4.0.17 (#29007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-15 15:46:10 +02:00
Petar Petrov
42e02be928 Add subpage titles in for config panel pages (#28990)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-01-15 12:48:33 +01:00
Aydar Gumerbaev
6213b6cd2a Always use fallback for brands URL (#28994) 2026-01-15 11:42:17 +00:00
Aidan Timson
cd75c55392 Entity context: voice assistants expose entities (#28992)
* Entity context: voice assistants expose entities

* Load virtualiser

* Refactor filter entities, reduce duplicate renders

* Fix logic
2026-01-15 13:09:38 +02:00
Marcin Bauer
ca325020d7 Add Labs feature note to automation element picker (#28874)
Co-authored-by: Wendelin <w@pe8.at>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-01-15 11:09:11 +00:00
Jeremy Cook
6250402661 Fix vertical-align in markdown tables with presentation role (#29001) 2026-01-15 11:48:43 +01:00
Amit Finkelstein
0bfca79851 Stop dropdown select events from bubbling in automation rows (#28985) 2026-01-15 11:22:23 +01:00
38 changed files with 621 additions and 347 deletions

View File

@@ -19,8 +19,11 @@ jobs:
release:
name: Release
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: write # Required to upload release assets
id-token: write # For "Trusted Publisher" to PyPi
if: github.repository_owner == 'home-assistant'
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
@@ -46,14 +49,18 @@ jobs:
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build and release package
run: |
python3 -m pip install twine build
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
python3 -m pip install build
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
script/release
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
skip-existing: true
- name: Upload release assets
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:

View File

@@ -176,7 +176,7 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.16",
"@vitest/coverage-v8": "4.0.17",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
@@ -217,7 +217,7 @@
"typescript": "5.9.3",
"typescript-eslint": "8.52.0",
"vite-tsconfig-paths": "6.0.4",
"vitest": "4.0.16",
"vitest": "4.0.17",
"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

@@ -1,5 +1,4 @@
#!/bin/sh
# Pushes a new version to PyPi.
# Stop on errors
set -e
@@ -12,5 +11,4 @@ yarn install
script/build_frontend
rm -rf dist home_assistant_frontend.egg-info
python3 -m build
python3 -m twine upload dist/*.whl --skip-existing
python3 -m build -q

View File

@@ -1,9 +1,9 @@
import { expose } from "comlink";
import Fuse, { type FuseOptionKey } from "fuse.js";
import type { FuseOptionKey, IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import memoizeOne from "memoize-one";
import { ipCompare, stringCompare } from "../../common/string/compare";
import { stripDiacritics } from "../../common/string/strip-diacritics";
import { multiTermSearch } from "../../resources/fuseMultiTerm";
import type {
ClonedDataTableColumnData,
DataTableRowData,
@@ -11,46 +11,159 @@ import type {
SortingDirection,
} from "./ha-data-table";
const getSearchKeys = memoizeOne(
(columns: SortableColumnContainer): FuseOptionKey<DataTableRowData>[] => {
const searchKeys = new Set<string>();
interface FilterKeyConfig {
key: string;
filterKey?: string;
}
Object.entries(columns).forEach(([key, column]) => {
if (column.filterable) {
searchKeys.add(
column.filterKey
? `${column.valueColumn || key}.${column.filterKey}`
: key
);
}
});
return Array.from(searchKeys);
const getFilterKeys = memoizeOne(
(columns: SortableColumnContainer): FilterKeyConfig[] =>
Object.entries(columns)
.filter(([, column]) => column.filterable)
.map(([key, column]) => ({
key: column.valueColumn || key,
filterKey: column.filterKey,
}))
);
const getSearchableValue = (
row: DataTableRowData,
{ key, filterKey }: FilterKeyConfig
): string => {
let value = row[key];
if (value == null) {
return "";
}
);
const fuseIndex = memoizeOne(
(data: DataTableRowData[], keys: FuseOptionKey<DataTableRowData>[]) =>
Fuse.createIndex(keys, data)
);
if (filterKey && typeof value === "object" && !Array.isArray(value)) {
value = value[filterKey];
if (value == null) {
return "";
}
}
if (Array.isArray(value)) {
const stringValues = value
.filter((item) => item != null && typeof item !== "object")
.map(String);
return stripDiacritics(stringValues.join(" ").toLowerCase());
}
return stripDiacritics(String(value).toLowerCase());
};
/** Filters data using exact substring matching (all terms must match). */
const filterDataExact = (
data: DataTableRowData[],
filterKeys: FilterKeyConfig[],
terms: string[]
): DataTableRowData[] => {
if (terms.length === 1) {
const term = terms[0];
return data.filter((row) =>
filterKeys.some((config) =>
getSearchableValue(row, config).includes(term)
)
);
}
return data.filter((row) => {
const searchString = filterKeys
.map((config) => getSearchableValue(row, config))
.join(" ");
return terms.every((term) => searchString.includes(term));
});
};
const FUZZY_OPTIONS: IFuseOptions<DataTableRowData> = {
ignoreDiacritics: true,
isCaseSensitive: false,
threshold: 0.2, // Stricter than default 0.3
minMatchCharLength: 2,
ignoreLocation: true,
shouldSort: false,
};
interface FuseKeyConfig {
name: string | string[];
getFn: (row: DataTableRowData) => string;
}
/** Filters data using fuzzy matching with Fuse.js (all terms must match). */
const filterDataFuzzy = (
data: DataTableRowData[],
filterKeys: FilterKeyConfig[],
terms: string[]
): DataTableRowData[] => {
// Build Fuse.js search keys from filter keys
const fuseKeys: FuseKeyConfig[] = filterKeys.map((config) => ({
name: config.filterKey ? [config.key, config.filterKey] : config.key,
getFn: (row: DataTableRowData) => getSearchableValue(row, config),
}));
// Find minimum term length to adjust minMatchCharLength
const minTermLength = Math.min(...terms.map((t) => t.length));
const minMatchCharLength = Math.min(minTermLength, 2);
const fuse = new Fuse<DataTableRowData>(data, {
...FUZZY_OPTIONS,
keys: fuseKeys as FuseOptionKey<DataTableRowData>[],
minMatchCharLength,
});
// For single term, simple search
if (terms.length === 1) {
return fuse.search(terms[0]).map((r) => r.item);
}
// For multiple terms, all must match (AND logic)
const expression = {
$and: terms.map((term) => ({
$or: fuseKeys.map((key) => ({
$path: Array.isArray(key.name) ? key.name : [key.name],
$val: term,
})),
})),
};
return fuse.search(expression).map((r) => r.item);
};
/**
* Filters data with exact match priority and fuzzy fallback.
* - First tries exact substring matching
* - If exact matches found, returns only those
* - If no exact matches, falls back to fuzzy search with strict scoring
*/
const filterData = (
data: DataTableRowData[],
columns: SortableColumnContainer,
filter: string
) => {
filter = stripDiacritics(filter.toLowerCase());
): DataTableRowData[] => {
const normalizedFilter = stripDiacritics(filter.toLowerCase().trim());
if (filter === "") {
if (!normalizedFilter) {
return data;
}
const keys = getSearchKeys(columns);
const filterKeys = getFilterKeys(columns);
const index = fuseIndex(data, keys);
if (!filterKeys.length) {
return data;
}
return multiTermSearch<DataTableRowData>(data, filter, keys, index, {
threshold: 0.2, // reduce fuzzy matches in data tables
});
const terms = normalizedFilter.split(/\s+/);
// First, try exact substring matching
const exactMatches = filterDataExact(data, filterKeys, terms);
if (exactMatches.length > 0) {
return exactMatches;
}
// No exact matches, fall back to fuzzy search
return filterDataFuzzy(data, filterKeys, terms);
};
const sortData = (

View File

@@ -138,10 +138,10 @@ export class HaMarkdown extends LitElement {
--markdown-table-padding-inline: 0;
--markdown-table-padding-block: 0;
th {
vertical-align: attr(align, center);
vertical-align: attr(valign, middle);
}
td {
vertical-align: attr(align, left);
vertical-align: attr(valign, middle);
}
}
table {

View File

@@ -223,7 +223,6 @@ export class HaRelatedItems extends LitElement {
.src=${brandsUrl({
domain: entry.domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
@@ -249,7 +248,6 @@ export class HaRelatedItems extends LitElement {
.src=${brandsUrl({
domain: integration,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"

View File

@@ -87,7 +87,6 @@ export class HaMediaSelector extends LitElement {
this._thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnail),
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
});
} else {

View File

@@ -589,10 +589,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
// On keypresses on the listbox, we're going to ignore mouse enter events
// for 100ms so that we ignore it when pressing down arrow scrolls the
// sidebar causing the mouse to hover a new icon
if (
this.alwaysExpand ||
new Date().getTime() < this._recentKeydownActiveUntil
) {
if (new Date().getTime() < this._recentKeydownActiveUntil) {
return;
}
if (this._mouseLeaveTimeout) {
@@ -612,7 +609,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
private _listboxFocusIn(ev) {
if (this.alwaysExpand || ev.target.localName !== "ha-md-list-item") {
if (ev.target.localName !== "ha-md-list-item") {
return;
}
this._showTooltip(ev.target);
@@ -652,6 +649,14 @@ class HaSidebar extends SubscribeMixin(LitElement) {
clearTimeout(this._tooltipHideTimeout);
this._tooltipHideTimeout = undefined;
}
const itemText = item.querySelector(".item-text") as HTMLElement | null;
if (this.hasAttribute("expanded") && itemText) {
const isTruncated = itemText.scrollWidth > itemText.clientWidth;
if (!isTruncated) {
this._hideTooltip();
return;
}
}
const tooltip = this._tooltip;
const allListbox = this.shadowRoot!.querySelectorAll("ha-md-list")!;
const listbox = [...allListbox].find((lb) => lb.contains(item));
@@ -662,9 +667,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
(listbox?.offsetTop ?? 0) -
(listbox?.scrollTop ?? 0);
tooltip.innerText = (
item.querySelector(".item-text") as HTMLElement
).innerText;
tooltip.innerText = itemText?.innerText ?? "";
tooltip.style.display = "block";
tooltip.style.position = "fixed";
tooltip.style.top = `${top}px`;
@@ -846,6 +849,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
:host([expanded]) ha-md-list-item .item-text {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.divider {
@@ -913,7 +919,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
position: absolute;
opacity: 0.9;
border-radius: var(--ha-border-radius-sm);
white-space: nowrap;
max-width: calc(var(--ha-space-20) * 3);
white-space: normal;
overflow-wrap: break-word;
color: var(--sidebar-background-color);
background-color: var(--sidebar-text-color);
padding: var(--ha-space-1);

View File

@@ -793,7 +793,6 @@ export class HaMediaPlayerBrowse extends LitElement {
thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnailUrl),
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
});
}

View File

@@ -11,6 +11,8 @@ import {
mdiViewDashboard,
} from "@mdi/js";
import type { HomeAssistant, PanelInfo } from "../types";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { LocalizeKeys } from "../common/translations/localize";
/** Panel to show when no panel is picked. */
export const DEFAULT_PANEL = "lovelace";
@@ -72,6 +74,40 @@ export const getPanelTitleFromUrlPath = (
return getPanelTitle(hass, panel);
};
/**
* Get subpage title for config panel routes.
* Returns the specific subpage title (e.g., "Automations") if found,
* or undefined to fall back to the panel title (e.g., "Settings").
*
* @param hass HomeAssistant instance
* @param path Full route path (e.g., "/config/automation/dashboard")
* @param configSections Config sections metadata for resolving subpage titles
* @returns Localized subpage title, or undefined if not found
*/
export const getConfigSubpageTitle = (
hass: HomeAssistant,
path: string,
configSections: Record<string, PageNavigation[]>
): string | undefined => {
// Search through all config section groups for a matching path
for (const sectionGroup of Object.values(configSections)) {
const pageNav = sectionGroup.find((nav) => path.startsWith(nav.path));
if (pageNav) {
if (pageNav.translationKey) {
const localized = hass.localize(pageNav.translationKey as LocalizeKeys);
if (localized) {
return localized;
}
}
if (pageNav.name) {
return pageNav.name;
}
}
}
return undefined;
};
export const getPanelIcon = (panel: PanelInfo): string | undefined => {
if (!panel.icon) {
switch (panel.component_name) {

View File

@@ -863,6 +863,7 @@ export default class HaAutomationActionRow extends LitElement {
}
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
ev.stopPropagation();
const action = ev.detail?.item?.value;
if (!action) {

View File

@@ -436,6 +436,24 @@ class DialogAddAutomationElement
// #region render
private _getEmptyNote(automationElementType: string) {
if (
automationElementType !== "trigger" &&
automationElementType !== "condition"
) {
return undefined;
}
return this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.no_items_for_target_note`,
{
labs_link: html`<a href="/config/labs" @click=${this._close}
>${this.hass.localize("ui.panel.config.labs.caption")}</a
>`,
}
);
}
protected render() {
if (!this._params) {
return nothing;
@@ -701,6 +719,7 @@ class DialogAddAutomationElement
.emptyLabel=${this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.no_items_for_target`
)}
.emptyNote=${this._getEmptyNote(automationElementType)}
.tooltipDescription=${this._tab === "targets"}
.target=${(this._tab === "targets" &&
this._selectedTarget &&
@@ -1696,9 +1715,9 @@ class DialogAddAutomationElement
// #region interaction
private _close() {
private _close = () => {
this._open = false;
}
};
private _back() {
mainWindow.history.back();

View File

@@ -1,5 +1,5 @@
import { mdiInformationOutline, mdiPlus } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { LitElement, css, html, nothing, type TemplateResult } from "lit";
import {
customElement,
eventOptions,
@@ -39,6 +39,8 @@ export class HaAutomationAddItems extends LitElement {
@property({ attribute: "empty-label" }) public emptyLabel!: string;
@property({ attribute: false }) public emptyNote?: string | TemplateResult;
@property({ attribute: false }) public target?: Target;
@property({ attribute: false }) public getLabel!: (
@@ -79,6 +81,9 @@ export class HaAutomationAddItems extends LitElement {
? html`${this.emptyLabel}
${this.target
? html`<div>${this._renderTarget(this.target)}</div>`
: nothing}
${this.emptyNote
? html`<div class="empty-note">${this.emptyNote}</div>`
: nothing}`
: repeat(
this.items,
@@ -199,6 +204,7 @@ export class HaAutomationAddItems extends LitElement {
static styles = css`
:host {
display: flex;
flex-grow: 1;
}
:host([scrollable]) .items {
overflow: auto;
@@ -213,13 +219,24 @@ export class HaAutomationAddItems extends LitElement {
background-color: var(--ha-color-surface-default);
align-items: center;
color: var(--ha-color-text-secondary);
padding: 0;
padding: var(--ha-space-4);
margin: 0 var(--ha-space-4)
max(var(--safe-area-inset-bottom), var(--ha-space-3));
line-height: var(--ha-line-height-expanded);
justify-content: center;
}
.empty-note {
color: var(--ha-color-text-secondary);
margin-top: var(--ha-space-2);
text-align: center;
}
.empty-note a {
color: currentColor;
text-decoration: underline;
}
.items.error {
background-color: var(--ha-color-fill-danger-quiet-resting);
color: var(--ha-color-on-danger-normal);

View File

@@ -839,6 +839,7 @@ export default class HaAutomationConditionRow extends LitElement {
}
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
ev.stopPropagation();
const action = ev.detail?.item?.value;
if (!action) {

View File

@@ -350,6 +350,7 @@ export default class HaAutomationOptionRow extends LitElement {
}
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
ev.stopPropagation();
const action = ev.detail?.item?.value;
if (!action) {

View File

@@ -815,6 +815,7 @@ export default class HaAutomationTriggerRow extends LitElement {
}
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
ev.stopPropagation();
const action = ev.detail?.item?.value;
if (!action) {

View File

@@ -152,7 +152,6 @@ class HaBackupConfigAgents extends LitElement {
.src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"

View File

@@ -66,7 +66,6 @@ class HaBackupAgentsPicker extends LitElement {
.src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"

View File

@@ -226,7 +226,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
.src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
height="24"

View File

@@ -207,7 +207,6 @@ class HaConfigBackupDetails extends LitElement {
.src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized:
this.hass.themes?.darkMode,
})}

View File

@@ -252,7 +252,6 @@ class HaConfigBackupSettings extends LitElement {
.src=${brandsUrl({
domain: "cloud",
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"

View File

@@ -4,10 +4,8 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-icon-picker";
import "../../../components/ha-settings-row";
import "../../../components/ha-textfield";
import "../../../components/ha-wa-dialog";
import "../../../components/ha-dialog-footer";
import { updateEntityRegistryEntry } from "../../../data/entity/entity_registry";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@@ -28,14 +26,21 @@ class DialogAssignCategory extends LitElement {
@state() private _submitting?: boolean;
@state() private _open = false;
public showDialog(params: AssignCategoryDialogParams): void {
this._params = params;
this._scope = params.scope;
this._category = params.entityReg.categories[params.scope];
this._error = undefined;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._error = "";
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -47,47 +52,46 @@ class DialogAssignCategory extends LitElement {
}
const entry = this._params.entityReg.categories[this._params.scope];
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
entry
? this.hass.localize("ui.panel.config.category.assign.edit")
: this.hass.localize("ui.panel.config.category.assign.assign")
)}
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${entry
? this.hass.localize("ui.panel.config.category.assign.edit")
: this.hass.localize("ui.panel.config.category.assign.assign")}
@closed=${this._dialogClosed}
>
<div>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
<ha-category-picker
.hass=${this.hass}
.scope=${this._scope}
.label=${this.hass.localize(
"ui.components.category-picker.category"
)}
.value=${this._category}
@value-changed=${this._categoryChanged}
></ha-category-picker>
</div>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
<ha-category-picker
.hass=${this.hass}
.scope=${this._scope}
.label=${this.hass.localize(
"ui.components.category-picker.category"
)}
.value=${this._category}
@value-changed=${this._categoryChanged}
autofocus
></ha-category-picker>
</div>
<ha-button
appearance="plain"
slot="primaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${!!this._submitting}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${!!this._submitting}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}

View File

@@ -3,9 +3,9 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-wa-dialog";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-icon-picker";
import "../../../components/ha-settings-row";
import "../../../components/ha-button";
import "../../../components/ha-textfield";
import type {
@@ -30,11 +30,14 @@ class DialogCategoryDetail extends LitElement {
@state() private _submitting?: boolean;
@state() private _open = false;
public async showDialog(
params: CategoryRegistryDetailDialogParams
): Promise<void> {
this._params = params;
this._error = undefined;
this._open = true;
if (this._params.entry) {
this._name = this._params.entry.name || "";
this._icon = this._params.entry.icon || null;
@@ -46,6 +49,10 @@ class DialogCategoryDetail extends LitElement {
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._error = "";
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -58,61 +65,55 @@ class DialogCategoryDetail extends LitElement {
const entry = this._params.entry;
const nameInvalid = !this._isNameValid();
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
entry
? this.hass.localize("ui.panel.config.category.editor.edit")
: this.hass.localize("ui.panel.config.category.editor.create")
)}
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${entry
? this.hass.localize("ui.panel.config.category.editor.edit")
: this.hass.localize("ui.panel.config.category.editor.create")}
@closed=${this._dialogClosed}
>
<div>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
<ha-textfield
.value=${this._name}
@input=${this._nameChanged}
.label=${this.hass.localize(
"ui.panel.config.category.editor.name"
)}
.validationMessage=${this.hass.localize(
"ui.panel.config.category.editor.required_error_msg"
)}
required
dialogInitialFocus
></ha-textfield>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
<ha-textfield
.value=${this._name}
@input=${this._nameChanged}
.label=${this.hass.localize("ui.panel.config.category.editor.name")}
.validationMessage=${this.hass.localize(
"ui.panel.config.category.editor.required_error_msg"
)}
required
autofocus
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
.value=${this._icon}
@value-changed=${this._iconChanged}
.label=${this.hass.localize(
"ui.panel.config.category.editor.icon"
)}
></ha-icon-picker>
</div>
<ha-icon-picker
.hass=${this.hass}
.value=${this._icon}
@value-changed=${this._iconChanged}
.label=${this.hass.localize("ui.panel.config.category.editor.icon")}
></ha-icon-picker>
</div>
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${nameInvalid || !!this._submitting}
>
${entry
? this.hass.localize("ui.common.save")
: this.hass.localize("ui.common.add")}
</ha-button>
</ha-dialog>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${nameInvalid || !!this._submitting}
>
${entry
? this.hass.localize("ui.common.save")
: this.hass.localize("ui.common.add")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}

View File

@@ -227,7 +227,6 @@ export class DialogHelperDetail extends LitElement {
src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"

View File

@@ -60,7 +60,6 @@ class HaDomainIntegrations extends LitElement {
src=${brandsUrl({
domain: flow.handler,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
@@ -106,7 +105,6 @@ class HaDomainIntegrations extends LitElement {
src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
@@ -170,7 +168,6 @@ class HaDomainIntegrations extends LitElement {
src=${brandsUrl({
domain: this.domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"

View File

@@ -57,7 +57,6 @@ export class HaIntegrationListItem extends ListItemBase {
src=${brandsUrl({
domain: this.integration.domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
brand: this.brand,
})}

View File

@@ -203,7 +203,6 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
src=${brandsUrl({
domain: preview_feature.domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"

View File

@@ -175,24 +175,31 @@ export class HaConfigLovelaceDashboards extends LitElement {
template: narrow
? undefined
: (dashboard) => html`
${dashboard.title}
${dashboard.default
? html`
<ha-svg-icon
.id="default-icon-${dashboard.title}"
style="padding-left: 10px; padding-inline-start: 10px; padding-inline-end: initial; direction: var(--direction);"
.path=${mdiHomeCircleOutline}
></ha-svg-icon>
<ha-tooltip
.for="default-icon-${dashboard.title}"
placement="right"
>
${this.hass.localize(
`ui.panel.config.lovelace.dashboards.default_dashboard`
)}
</ha-tooltip>
`
: nothing}
<span
style="display:flex; align-items:center; gap: var(--ha-space-2); min-width:0; width:100%;"
>
<span
style="min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1;"
>${dashboard.title}</span
>
${dashboard.default
? html`
<ha-svg-icon
.id="default-icon-${dashboard.title}"
style="flex-shrink:0;"
.path=${mdiHomeCircleOutline}
></ha-svg-icon>
<ha-tooltip
.for="default-icon-${dashboard.title}"
placement="right"
>
${this.hass.localize(
`ui.panel.config.lovelace.dashboards.default_dashboard`
)}
</ha-tooltip>
`
: nothing}
</span>
`,
},
};

View File

@@ -75,7 +75,6 @@ class HaConfigRepairs extends LitElement {
src=${brandsUrl({
domain: issue.issue_domain || issue.domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
.title=${domainName}

View File

@@ -58,7 +58,6 @@ class IntegrationsStartupTime extends LitElement {
src=${brandsUrl({
domain: setup.domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"

View File

@@ -8,6 +8,8 @@ import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { computeEntityNameList } from "../../../common/entity/compute_entity_name_display";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-check-list-item";
import "../../../components/search-input";
import "../../../components/ha-dialog";
@@ -18,10 +20,16 @@ import "../../../components/ha-list";
import type { ExposeEntitySettings } from "../../../data/expose";
import { voiceAssistants } from "../../../data/expose";
import { haStyle } from "../../../resources/styles";
import { loadVirtualizer } from "../../../resources/virtualizer";
import type { HomeAssistant } from "../../../types";
import "./entity-voice-settings";
import type { ExposeEntityDialogParams } from "./show-dialog-expose-entity";
interface FilteredEntity {
entity: HassEntity;
nameList: (string | undefined)[];
}
@customElement("dialog-expose-entity")
class DialogExposeEntity extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -32,6 +40,12 @@ class DialogExposeEntity extends LitElement {
@state() private _selected: string[] = [];
public willUpdate(): void {
if (!this.hasUpdated) {
loadVirtualizer();
}
}
public async showDialog(params: ExposeEntityDialogParams): Promise<void> {
this._params = params;
}
@@ -141,38 +155,101 @@ class DialogExposeEntity extends LitElement {
(
exposedEntities: Record<string, ExposeEntitySettings>,
filter?: string
) => {
): FilteredEntity[] => {
const lowerFilter = filter?.toLowerCase();
return Object.values(this.hass.states).filter(
(entity) =>
this._params!.filterAssistants.some(
(ass) => !exposedEntities[entity.entity_id]?.[ass]
) &&
(!lowerFilter ||
entity.entity_id.toLowerCase().includes(lowerFilter) ||
computeStateName(entity)?.toLowerCase().includes(lowerFilter))
);
const result: FilteredEntity[] = [];
for (const entity of Object.values(this.hass.states)) {
if (
this._params!.filterAssistants.every(
(ass) => exposedEntities[entity.entity_id]?.[ass]
)
) {
continue;
}
const nameList = computeEntityNameList(
entity,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
if (!lowerFilter) {
result.push({ entity, nameList });
continue;
}
if (entity.entity_id.toLowerCase().includes(lowerFilter)) {
result.push({ entity, nameList });
continue;
}
const entityName = computeStateName(entity);
if (entityName?.toLowerCase().includes(lowerFilter)) {
result.push({ entity, nameList });
continue;
}
const [, deviceName, areaName] = nameList;
if (deviceName?.toLowerCase().includes(lowerFilter)) {
result.push({ entity, nameList });
continue;
}
if (areaName?.toLowerCase().includes(lowerFilter)) {
result.push({ entity, nameList });
continue;
}
}
return result;
}
);
private _renderItem = (entityState: HassEntity) => html`
<ha-check-list-item
graphic="icon"
twoLine
.value=${entityState.entity_id}
.selected=${this._selected.includes(entityState.entity_id)}
@request-selected=${this._handleSelected}
>
<ha-state-icon
title=${ifDefined(entityState?.state)}
slot="graphic"
.hass=${this.hass}
.stateObj=${entityState}
></ha-state-icon>
${computeStateName(entityState)}
<span slot="secondary">${entityState.entity_id}</span>
</ha-check-list-item>
`;
private _renderItem = (item: FilteredEntity) => {
const { entity: entityState, nameList } = item;
const [entityName, deviceName, areaName] = nameList;
const isRTL = computeRTL(this.hass);
const primary = entityName || deviceName || entityState.entity_id;
const context = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const showEntityId = this.hass.userData?.showEntityIdPicker;
return html`
<ha-check-list-item
graphic="icon"
?twoLine=${context}
?threeLine=${showEntityId}
.value=${entityState.entity_id}
.selected=${this._selected.includes(entityState.entity_id)}
@request-selected=${this._handleSelected}
>
<ha-state-icon
title=${ifDefined(entityState?.state)}
slot="graphic"
.hass=${this.hass}
.stateObj=${entityState}
></ha-state-icon>
${primary}
${context || showEntityId
? html`<span slot="secondary">
${context}
${showEntityId
? html`<br /><span class="entity-id"
>${entityState.entity_id}</span
>`
: nothing}
</span>`
: nothing}
</ha-check-list-item>
`;
};
private _expose() {
this._params!.exposeEntities(this._selected);
@@ -198,6 +275,7 @@ class DialogExposeEntity extends LitElement {
width: 100%;
display: block;
box-sizing: border-box;
margin-top: var(--ha-space-2);
--text-field-suffix-padding-left: 8px;
}
.header {
@@ -210,7 +288,7 @@ class DialogExposeEntity extends LitElement {
box-sizing: border-box;
display: flex;
flex-direction: column;
margin: -4px 0;
margin: calc(var(--ha-space-1) * -1) 0;
}
.subtitle {
color: var(--secondary-text-color);
@@ -225,9 +303,17 @@ class DialogExposeEntity extends LitElement {
width: 100%;
height: 72px;
}
ha-check-list-item[threeLine] {
height: 88px;
}
ha-check-list-item .entity-id {
line-height: var(--ha-line-height-normal);
padding-left: var(--ha-space-1);
font-size: var(--ha-font-size-xs);
}
ha-check-list-item ha-state-icon {
margin-left: 24px;
margin-inline-start: 24px;
margin-left: var(--ha-space-6);
margin-inline-start: var(--ha-space-6);
margin-inline-end: initial;
}
@media all and (max-height: 800px) {
@@ -262,8 +348,8 @@ class DialogExposeEntity extends LitElement {
--text-field-suffix-padding-left: unset;
}
ha-check-list-item ha-state-icon {
margin-left: 8px;
margin-inline-start: 8px;
margin-left: var(--ha-space-2);
margin-inline-start: var(--ha-space-2);
margin-inline-end: initial;
}
}

View File

@@ -167,20 +167,21 @@ export class VoiceAssistantsExpose extends LitElement {
filterable: true,
direction: "asc",
flex: 2,
template: narrow
? undefined
: (entry) => html`
${entry.name}<br />
<div class="secondary">${entry.entity_id}</div>
`,
},
// For search & narrow
area: {
title: localize("ui.panel.config.voice_assistants.expose.headers.area"),
sortable: true,
groupable: true,
filterable: true,
template: (entry) => entry.area || "—",
},
entity_id: {
title: localize(
"ui.panel.config.voice_assistants.expose.headers.entity_id"
),
hidden: !narrow,
sortable: true,
filterable: true,
defaultHidden: true,
},
domain: {
title: localize(
@@ -191,13 +192,6 @@ export class VoiceAssistantsExpose extends LitElement {
filterable: true,
groupable: true,
},
area: {
title: localize("ui.panel.config.voice_assistants.expose.headers.area"),
sortable: true,
groupable: true,
filterable: true,
template: (entry) => entry.area || "—",
},
assistants: {
title: localize(
"ui.panel.config.voice_assistants.expose.headers.assistants"
@@ -819,8 +813,8 @@ export class VoiceAssistantsExpose extends LitElement {
}
.selected-txt {
font-weight: var(--ha-font-weight-bold);
padding-left: 16px;
padding-inline-start: 16px;
padding-left: var(--ha-space-4);
padding-inline-start: var(--ha-space-4);
direction: var(--direction);
}
.table-header .selected-txt {
@@ -830,8 +824,8 @@ export class VoiceAssistantsExpose extends LitElement {
font-size: var(--ha-font-size-l);
}
.header-toolbar .header-btns {
margin-right: -12px;
margin-inline-end: -12px;
margin-right: calc(var(--ha-space-3) * -1);
margin-inline-end: calc(var(--ha-space-3) * -1);
direction: var(--direction);
}
.header-btns {
@@ -839,17 +833,17 @@ export class VoiceAssistantsExpose extends LitElement {
}
.header-btns > ha-button,
.header-btns > ha-icon-button {
margin: 8px;
margin: var(--ha-space-2);
}
ha-button-menu {
margin-left: 8px;
margin-inline-start: 8px;
margin-left: var(--ha-space-2);
margin-inline-start: var(--ha-space-2);
margin-inline-end: initial;
}
.clear {
color: var(--primary-color);
padding-left: 8px;
padding-inline-start: 8px;
padding-left: var(--ha-space-2);
padding-inline-start: var(--ha-space-2);
text-transform: uppercase;
direction: var(--direction);
}

View File

@@ -160,7 +160,6 @@ class HaLogbookRenderer extends LitElement {
? brandsUrl({
domain: domain!,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})
: undefined;

View File

@@ -1,30 +1,55 @@
import { getPanelTitleFromUrlPath } from "../data/panel";
import type { PropertyValues } from "lit";
import { getConfigSubpageTitle, getPanelTitleFromUrlPath } from "../data/panel";
import { configSections } from "../panels/config/ha-panel-config";
import type { Constructor, HomeAssistant } from "../types";
import type { HassBaseEl } from "./hass-base-mixin";
const setTitle = (title: string | undefined) => {
const setPageTitle = (title: string | undefined) => {
document.title = title ? `${title} Home Assistant` : "Home Assistant";
};
const getRoutePath = (): string =>
// In demo mode, use hash; otherwise use pathname
__DEMO__ ? window.location.hash.substring(1) : window.location.pathname;
export const panelTitleMixin = <T extends Constructor<HassBaseEl>>(
superClass: T
) =>
class extends superClass {
protected updated(changedProps) {
private _previousPath?: string;
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!changedProps.has("hass") || !this.hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const currentPath = getRoutePath();
// Update title when panel, localize, or route path changes
if (
!oldHass ||
oldHass.panels !== this.hass.panels ||
oldHass.panelUrl !== this.hass.panelUrl ||
oldHass.localize !== this.hass.localize
oldHass.localize !== this.hass.localize ||
this._previousPath !== currentPath
) {
setTitle(getPanelTitleFromUrlPath(this.hass, this.hass.panelUrl));
this._previousPath = currentPath;
let title: string | undefined;
// Try to get specific subpage title for config panel
if (this.hass.panelUrl === "config") {
title = getConfigSubpageTitle(this.hass, currentPath, configSections);
}
// Fall back to panel title
if (!title) {
title = getPanelTitleFromUrlPath(this.hass, this.hass.panelUrl);
}
setPageTitle(title);
}
}
};

View File

@@ -4162,6 +4162,7 @@
"cut_to_clipboard": "Trigger cut to clipboard",
"select": "Select a trigger",
"no_items_for_target": "No triggers available for",
"no_items_for_target_note": "This is a {labs_link} feature. More triggers will be added in future updates.",
"groups": {
"device": {
"label": "Device"
@@ -4433,6 +4434,7 @@
"cut_to_clipboard": "Condition cut to clipboard",
"select": "Select a condition",
"no_items_for_target": "No conditions available for",
"no_items_for_target_note": "This is a {labs_link} feature. More conditions will be added in future updates.",
"groups": {
"device": {
"label": "Device"

View File

@@ -1,7 +1,6 @@
export interface BrandsOptions {
domain: string;
type: "icon" | "logo" | "icon@2x" | "logo@2x";
useFallback?: boolean;
darkOptimized?: boolean;
brand?: boolean;
}
@@ -14,11 +13,9 @@ export interface HardwareBrandsOptions {
}
export const brandsUrl = (options: BrandsOptions): string =>
`https://brands.home-assistant.io/${options.brand ? "brands/" : ""}${
options.useFallback ? "_/" : ""
}${options.domain}/${options.darkOptimized ? "dark_" : ""}${
options.type
}.png`;
`https://brands.home-assistant.io/${options.brand ? "brands/" : ""}_/${options.domain}/${
options.darkOptimized ? "dark_" : ""
}${options.type}.png`;
export const hardwareBrandsUrl = (options: HardwareBrandsOptions): string =>
`https://brands.home-assistant.io/hardware/${options.category}/${

View File

@@ -2,40 +2,26 @@ import { assert, describe, it } from "vitest";
import { brandsUrl } from "../../src/util/brands-url";
describe("Generate brands Url", () => {
it("Generate logo brands url for cloud component without fallback", () => {
it("Generate logo brands url for cloud component", () => {
assert.strictEqual(
// @ts-ignore
brandsUrl({ domain: "cloud", type: "logo" }),
"https://brands.home-assistant.io/cloud/logo.png"
);
});
it("Generate icon brands url for cloud component without fallback", () => {
assert.strictEqual(
// @ts-ignore
brandsUrl({ domain: "cloud", type: "icon" }),
"https://brands.home-assistant.io/cloud/icon.png"
);
});
it("Generate logo brands url for cloud component with fallback", () => {
assert.strictEqual(
// @ts-ignore
brandsUrl({ domain: "cloud", type: "logo", useFallback: true }),
"https://brands.home-assistant.io/_/cloud/logo.png"
);
});
it("Generate icon brands url for cloud component with fallback", () => {
it("Generate icon brands url for cloud component", () => {
assert.strictEqual(
// @ts-ignore
brandsUrl({ domain: "cloud", type: "icon", useFallback: true }),
brandsUrl({ domain: "cloud", type: "icon" }),
"https://brands.home-assistant.io/_/cloud/icon.png"
);
});
it("Generate dark theme optimized logo brands url for cloud component without fallback", () => {
it("Generate dark theme optimized logo brands url for cloud component", () => {
assert.strictEqual(
// @ts-ignore
brandsUrl({ domain: "cloud", type: "logo", darkOptimized: true }),
"https://brands.home-assistant.io/cloud/dark_logo.png"
"https://brands.home-assistant.io/_/cloud/dark_logo.png"
);
});
});

140
yarn.lock
View File

@@ -2191,7 +2191,7 @@ __metadata:
languageName: node
linkType: hard
"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28, @jridgewell/trace-mapping@npm:^0.3.31":
"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28, @jridgewell/trace-mapping@npm:^0.3.31":
version: 0.3.31
resolution: "@jridgewell/trace-mapping@npm:0.3.31"
dependencies:
@@ -5192,50 +5192,49 @@ __metadata:
languageName: node
linkType: hard
"@vitest/coverage-v8@npm:4.0.16":
version: 4.0.16
resolution: "@vitest/coverage-v8@npm:4.0.16"
"@vitest/coverage-v8@npm:4.0.17":
version: 4.0.17
resolution: "@vitest/coverage-v8@npm:4.0.17"
dependencies:
"@bcoe/v8-coverage": "npm:^1.0.2"
"@vitest/utils": "npm:4.0.16"
ast-v8-to-istanbul: "npm:^0.3.8"
"@vitest/utils": "npm:4.0.17"
ast-v8-to-istanbul: "npm:^0.3.10"
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"
obug: "npm:^2.1.1"
std-env: "npm:^3.10.0"
tinyrainbow: "npm:^3.0.3"
peerDependencies:
"@vitest/browser": 4.0.16
vitest: 4.0.16
"@vitest/browser": 4.0.17
vitest: 4.0.17
peerDependenciesMeta:
"@vitest/browser":
optional: true
checksum: 10/cfb0095db60baa7ec8f76d4a9d09a92f1d9f2bfe83adfbbfca32a9b1a6dd76447db33bbba2fd8882f551ca206ef86f74f6ed436643a036537ecea6398a6825f1
checksum: 10/aab6340670dbf42a5bf4a28b49a4d4c8819e842edac45567bae50af27b9e89264406945e57dd115b833190a6c25ba8f716c2eabaa23d2e249a185e3acc97ec1a
languageName: node
linkType: hard
"@vitest/expect@npm:4.0.16":
version: 4.0.16
resolution: "@vitest/expect@npm:4.0.16"
"@vitest/expect@npm:4.0.17":
version: 4.0.17
resolution: "@vitest/expect@npm:4.0.17"
dependencies:
"@standard-schema/spec": "npm:^1.0.0"
"@types/chai": "npm:^5.2.2"
"@vitest/spy": "npm:4.0.16"
"@vitest/utils": "npm:4.0.16"
"@vitest/spy": "npm:4.0.17"
"@vitest/utils": "npm:4.0.17"
chai: "npm:^6.2.1"
tinyrainbow: "npm:^3.0.3"
checksum: 10/1da98c86d394a4955bef381ac2c63a52d2eec0086f55e18858083da928cfdf51e7a30bfd88b1814e861906dae44d089aeab0fcc67b2597a4a8073c70cd14bdf7
checksum: 10/f260fefea527aae652be8d71ff188d45f958b7299a4577d1c3ed15bc87e6b20a6abb30ec6419c826259863d8bdbc1122e82cc499fb9eb63aaa43d3a5be1b7f76
languageName: node
linkType: hard
"@vitest/mocker@npm:4.0.16":
version: 4.0.16
resolution: "@vitest/mocker@npm:4.0.16"
"@vitest/mocker@npm:4.0.17":
version: 4.0.17
resolution: "@vitest/mocker@npm:4.0.17"
dependencies:
"@vitest/spy": "npm:4.0.16"
"@vitest/spy": "npm:4.0.17"
estree-walker: "npm:^3.0.3"
magic-string: "npm:^0.30.21"
peerDependencies:
@@ -5246,54 +5245,54 @@ __metadata:
optional: true
vite:
optional: true
checksum: 10/3a34c6571ef278b80d33feabb8389d6cf7cfd248fe592b8b2a373650ab460b95805fde65e6bd76aebc75729fc0c94b4d8b9bba25fa55e21c2745ae03c10316bf
checksum: 10/4d938c298dd7e63d23efc56a81e254a8a453b0157b378d4b7af57a40dd2687c24a0e1f2e2499f8d17fe302e6d6d515e67c6a5fbfbff75dee2cfd51c37cf4c7dc
languageName: node
linkType: hard
"@vitest/pretty-format@npm:4.0.16":
version: 4.0.16
resolution: "@vitest/pretty-format@npm:4.0.16"
"@vitest/pretty-format@npm:4.0.17":
version: 4.0.17
resolution: "@vitest/pretty-format@npm:4.0.17"
dependencies:
tinyrainbow: "npm:^3.0.3"
checksum: 10/914d5d35fb3b0aa67f8e6065ac3d1f1798b7774e1ad9d1e873e7c6efdc7925c98e0f8188bb13c4f3feb4d80b756c337f7a55cd4f78c50fe786330d0aaede7cfd
checksum: 10/e50925f44168b8108a5094e44fd739b7183457c101eb020e88b5556a2f857808d0c9d045113aec83815a20d4aaaf9b7a522a1c651ce111de18daa686891b37a0
languageName: node
linkType: hard
"@vitest/runner@npm:4.0.16":
version: 4.0.16
resolution: "@vitest/runner@npm:4.0.16"
"@vitest/runner@npm:4.0.17":
version: 4.0.17
resolution: "@vitest/runner@npm:4.0.17"
dependencies:
"@vitest/utils": "npm:4.0.16"
"@vitest/utils": "npm:4.0.17"
pathe: "npm:^2.0.3"
checksum: 10/2aed39bb46ba747bd4fd5acf081e9e500192fec19c1887399f6a1701bbfdab05f3d3b45c00e4af5b90a0832853c959a0f64e676b05c67f5457b7c6984f844aa2
checksum: 10/75c62ac09b506d2707baad72c9a8ca6addb9bb179548d9ec9af3f7f2303b2e03f4001480c9657325718b15f2997fc39168c027d8d88794c0f8c04800c640c055
languageName: node
linkType: hard
"@vitest/snapshot@npm:4.0.16":
version: 4.0.16
resolution: "@vitest/snapshot@npm:4.0.16"
"@vitest/snapshot@npm:4.0.17":
version: 4.0.17
resolution: "@vitest/snapshot@npm:4.0.17"
dependencies:
"@vitest/pretty-format": "npm:4.0.16"
"@vitest/pretty-format": "npm:4.0.17"
magic-string: "npm:^0.30.21"
pathe: "npm:^2.0.3"
checksum: 10/30f2977c96645c018b9d1f658e758f4f886ac63966dca909e9f736d6c9d6d0a6dabdeaedf9abcc13e1000458e4069283632c0140033972847dc1f4b4ac38e076
checksum: 10/0cda8970f484bdc5777347cc317f020dc7773ddf0cea996ab5fff453966310c64e9a97854b04998cf0635e8118c12e2235c7a5f921fdfc288dc63dc27c3116d8
languageName: node
linkType: hard
"@vitest/spy@npm:4.0.16":
version: 4.0.16
resolution: "@vitest/spy@npm:4.0.16"
checksum: 10/76cbabfdd77adf16904d5c128de67abca650bbc2ed36acc68fca548dc51844c7fc1ac516e384d07341b25ae39318c7c2feb499ffa7283a1a838f762cb0cda6ab
"@vitest/spy@npm:4.0.17":
version: 4.0.17
resolution: "@vitest/spy@npm:4.0.17"
checksum: 10/23313980c512b00c08a1c64f6ed15dc7c295bb7b09feab571a3cc96536de2f07432109256717f9deb7f1b8c9ba9ac28f7e617cf639654bc564f6ea5a341ad8f4
languageName: node
linkType: hard
"@vitest/utils@npm:4.0.16":
version: 4.0.16
resolution: "@vitest/utils@npm:4.0.16"
"@vitest/utils@npm:4.0.17":
version: 4.0.17
resolution: "@vitest/utils@npm:4.0.17"
dependencies:
"@vitest/pretty-format": "npm:4.0.16"
"@vitest/pretty-format": "npm:4.0.17"
tinyrainbow: "npm:^3.0.3"
checksum: 10/07fb3c96867656ff080df7ae6056a8dc23931d0f8bc16e15994c576c580dc6e2dcf71af0964fee197ea7eea4f4ad72c256f56cd3b81599f9e0ba63a228968d50
checksum: 10/b8b96f8c2c4fee13f4ef4927e56bbf98c2d4f3a61428d9721c5578c96e2a0953892dfccfad3e0c1a7b3105e3d24f93f826f8338c82c72b9f8bc32b50bc9072a1
languageName: node
linkType: hard
@@ -5775,14 +5774,14 @@ __metadata:
languageName: node
linkType: hard
"ast-v8-to-istanbul@npm:^0.3.8":
version: 0.3.9
resolution: "ast-v8-to-istanbul@npm:0.3.9"
"ast-v8-to-istanbul@npm:^0.3.10":
version: 0.3.10
resolution: "ast-v8-to-istanbul@npm:0.3.10"
dependencies:
"@jridgewell/trace-mapping": "npm:^0.3.31"
estree-walker: "npm:^3.0.3"
js-tokens: "npm:^9.0.1"
checksum: 10/7fd175232b898e7dfa9c576dd765fb60cce3a057ea7ab108a64c745c6fd201cddc7a529e6ab7967b294627e63e920e5a0e72dbc2348aa8ed53030da7c61d93b6
checksum: 10/240a5e2c24776b355f2442fa93564a528b8df4b8d94e9bc3234f25020ffac745886865a3a92e5e9dc67ee9720739ec078f04790a3607a7ad98d8349cf75ddf04
languageName: node
linkType: hard
@@ -9091,7 +9090,7 @@ __metadata:
"@types/ua-parser-js": "npm:0.7.39"
"@types/webspeechapi": "npm:0.0.29"
"@vibrant/color": "npm:4.0.0"
"@vitest/coverage-v8": "npm:4.0.16"
"@vitest/coverage-v8": "npm:4.0.17"
"@vue/web-component-wrapper": "npm:1.3.0"
"@webcomponents/scoped-custom-element-registry": "npm:0.0.10"
"@webcomponents/webcomponentsjs": "npm:2.8.0"
@@ -9176,7 +9175,7 @@ __metadata:
typescript-eslint: "npm:8.52.0"
ua-parser-js: "npm:2.0.7"
vite-tsconfig-paths: "npm:6.0.4"
vitest: "npm:4.0.16"
vitest: "npm:4.0.17"
vue: "npm:2.7.16"
vue2-daterange-picker: "npm:0.6.8"
webpack-stats-plugin: "npm:1.1.3"
@@ -10113,17 +10112,6 @@ __metadata:
languageName: node
linkType: hard
"istanbul-lib-source-maps@npm:^5.0.6":
version: 5.0.6
resolution: "istanbul-lib-source-maps@npm:5.0.6"
dependencies:
"@jridgewell/trace-mapping": "npm:^0.3.23"
debug: "npm:^4.1.1"
istanbul-lib-coverage: "npm:^3.0.0"
checksum: 10/569dd0a392ee3464b1fe1accbaef5cc26de3479eacb5b91d8c67ebb7b425d39fd02247d85649c3a0e9c29b600809fa60b5af5a281a75a89c01f385b1e24823a2
languageName: node
linkType: hard
"istanbul-reports@npm:^3.2.0":
version: 3.2.0
resolution: "istanbul-reports@npm:3.2.0"
@@ -14498,17 +14486,17 @@ __metadata:
languageName: node
linkType: hard
"vitest@npm:4.0.16":
version: 4.0.16
resolution: "vitest@npm:4.0.16"
"vitest@npm:4.0.17":
version: 4.0.17
resolution: "vitest@npm:4.0.17"
dependencies:
"@vitest/expect": "npm:4.0.16"
"@vitest/mocker": "npm:4.0.16"
"@vitest/pretty-format": "npm:4.0.16"
"@vitest/runner": "npm:4.0.16"
"@vitest/snapshot": "npm:4.0.16"
"@vitest/spy": "npm:4.0.16"
"@vitest/utils": "npm:4.0.16"
"@vitest/expect": "npm:4.0.17"
"@vitest/mocker": "npm:4.0.17"
"@vitest/pretty-format": "npm:4.0.17"
"@vitest/runner": "npm:4.0.17"
"@vitest/snapshot": "npm:4.0.17"
"@vitest/spy": "npm:4.0.17"
"@vitest/utils": "npm:4.0.17"
es-module-lexer: "npm:^1.7.0"
expect-type: "npm:^1.2.2"
magic-string: "npm:^0.30.21"
@@ -14526,10 +14514,10 @@ __metadata:
"@edge-runtime/vm": "*"
"@opentelemetry/api": ^1.9.0
"@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0
"@vitest/browser-playwright": 4.0.16
"@vitest/browser-preview": 4.0.16
"@vitest/browser-webdriverio": 4.0.16
"@vitest/ui": 4.0.16
"@vitest/browser-playwright": 4.0.17
"@vitest/browser-preview": 4.0.17
"@vitest/browser-webdriverio": 4.0.17
"@vitest/ui": 4.0.17
happy-dom: "*"
jsdom: "*"
peerDependenciesMeta:
@@ -14553,7 +14541,7 @@ __metadata:
optional: true
bin:
vitest: vitest.mjs
checksum: 10/22b3806988ab186be4a6a133903a70c62835198e8e749f6ed751957d23bc1e3f0466e310a1a79d0b70a354b2e308e574486191eb39711257b3fe61e4fe00d1c8
checksum: 10/792cf5ecdb2c0c2a61fc7beacec800413dcc5b68ad5e18f74795cdbfe513d58e3b6e437571c728c9992920f52d0640a5264aaf8c3702454b2637ff93451cf567
languageName: node
linkType: hard