Compare commits

...

20 Commits

Author SHA1 Message Date
Bram Kragten
ee82ec4987 Merge branch 'dev' into unassigned_devices 2025-12-23 14:44:46 +01:00
Wendelin
97d51094df Add iOS-specific autofocus handling in HaWaDialog (#28607) 2025-12-23 14:43:03 +01:00
Wendelin
0904a1116c Language picker: add search fallback to en (#27818) 2025-12-23 14:41:30 +01:00
Wendelin
282458e645 Automation editor target in row improve configEntry subscription (#28662) 2025-12-23 14:40:55 +01:00
Paul Bottein
063c2d776a Improve new color picker (#28663)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-12-23 10:36:00 +00:00
Aidan Timson
97e0bc8080 Show icons for ha-tab in desktop views (#28508)
Add icons to tabs
2025-12-22 21:18:54 +01:00
renovate[bot]
21e2c676b8 Update dependency @lokalise/node-api to v15.6.0 (#28668)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 20:50:46 +01:00
Paul Bottein
214b8cd5c7 Put favorite at the top for home dashboard (#28665) 2025-12-22 20:50:22 +01:00
renovate[bot]
3bd5481274 Update formatjs monorepo (#28667)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 20:39:18 +01:00
Wendelin
ac63f991b2 Use generic-picker for log provider select (#28664)
Use generic-picker for log provider switcher
2025-12-22 18:56:22 +01:00
renovate[bot]
97e9129832 Update dependency sinon to v21.0.1 (#28666)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 18:02:51 +01:00
Paul Bottein
cf95bf89c3 Use compute domain helper 2025-12-15 15:59:25 +01:00
Paul Bottein
6180ba0156 Move other devices out of summaries 2025-12-15 15:18:29 +01:00
Paul Bottein
a11b595435 Remove summary 2025-12-15 15:08:40 +01:00
Paul Bottein
d819f784ac Rename unassigned to other 2025-12-15 15:06:35 +01:00
Paul Bottein
c08d229757 Hide some platform and domains 2025-12-15 14:54:25 +01:00
Paul Bottein
ad481f0ad0 Revert "Add heading badge button"
This reverts commit d9d1d8caf0.
2025-12-15 13:45:08 +01:00
Paul Bottein
d9d1d8caf0 Add heading badge button 2025-12-15 13:44:40 +01:00
Paul Bottein
928d989c56 Add sections for entities and helpers 2025-12-15 13:44:40 +01:00
Paul Bottein
e39ad38efb Add unassigned devices view to home dashboard 2025-12-15 13:44:40 +01:00
23 changed files with 872 additions and 290 deletions

View File

@@ -37,15 +37,15 @@
"@codemirror/view": "6.39.4",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.0.2",
"@formatjs/intl-displaynames": "7.0.2",
"@formatjs/intl-durationformat": "0.8.2",
"@formatjs/intl-getcanonicallocales": "3.0.2",
"@formatjs/intl-listformat": "8.0.2",
"@formatjs/intl-locale": "5.0.2",
"@formatjs/intl-numberformat": "9.0.3",
"@formatjs/intl-pluralrules": "6.0.2",
"@formatjs/intl-relativetimeformat": "12.0.3",
"@formatjs/intl-datetimeformat": "7.0.4",
"@formatjs/intl-displaynames": "7.0.4",
"@formatjs/intl-durationformat": "0.8.4",
"@formatjs/intl-getcanonicallocales": "3.0.3",
"@formatjs/intl-listformat": "8.0.4",
"@formatjs/intl-locale": "5.0.4",
"@formatjs/intl-numberformat": "9.0.5",
"@formatjs/intl-pluralrules": "6.0.4",
"@formatjs/intl-relativetimeformat": "12.0.5",
"@fullcalendar/core": "6.1.19",
"@fullcalendar/daygrid": "6.1.19",
"@fullcalendar/interaction": "6.1.19",
@@ -112,7 +112,7 @@
"hls.js": "1.6.15",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "11.0.2",
"intl-messageformat": "11.0.4",
"js-yaml": "4.1.1",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -151,7 +151,7 @@
"@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.5",
"@bundle-stats/plugin-webpack-filter": "4.21.7",
"@lokalise/node-api": "15.5.0",
"@lokalise/node-api": "15.6.0",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
@@ -210,7 +210,7 @@
"prettier": "3.7.4",
"rspack-manifest-plugin": "5.2.0",
"serve": "14.2.5",
"sinon": "21.0.0",
"sinon": "21.0.1",
"tar": "7.5.2",
"terser-webpack-plugin": "5.3.16",
"ts-lit-plugin": "2.0.2",

View File

@@ -1,7 +1,6 @@
export const THEME_COLORS = new Set([
"primary",
"accent",
"disabled",
"red",
"pink",
"purple",
@@ -25,6 +24,9 @@ export const THEME_COLORS = new Set([
"blue-grey",
"black",
"white",
"primary-text",
"secondary-text",
"disabled",
]);
export function computeCssColor(color: string): string {

View File

@@ -15,6 +15,7 @@ export interface EntityFilter {
label?: string | string[];
entity_category?: EntityCategory | EntityCategory[];
hidden_platform?: string | string[];
hidden_domains?: string | string[];
}
export type EntityFilterFunc = (entityId: string) => boolean;
@@ -38,6 +39,9 @@ export const generateEntityFilter = (
const domains = filter.domain
? new Set(ensureArray(filter.domain))
: undefined;
const hiddenDomains = filter.hidden_domains
? new Set(ensureArray(filter.hidden_domains))
: undefined;
const deviceClasses = filter.device_class
? new Set(ensureArray(filter.device_class))
: undefined;
@@ -57,12 +61,16 @@ export const generateEntityFilter = (
if (!stateObj) {
return false;
}
if (domains) {
if (domains || hiddenDomains) {
const domain = computeDomain(entityId);
if (!domains.has(domain)) {
if (domains && !domains.has(domain)) {
return false;
}
if (hiddenDomains && hiddenDomains.has(domain)) {
return false;
}
}
if (deviceClasses) {
const dc = stateObj.attributes.device_class || "none";
if (!deviceClasses.has(dc)) {

View File

@@ -1,7 +1,8 @@
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
import { html, LitElement } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeKeys } from "../common/translations/localize";
@@ -49,11 +50,40 @@ export class HaColorPicker extends LitElement {
.rowRenderer=${this._rowRenderer}
.valueRenderer=${this._valueRenderer}
@value-changed=${this._valueChanged}
.notFoundLabel=${this.hass.localize(
"ui.components.color-picker.no_colors_found"
)}
.getAdditionalItems=${this._getAdditionalItems}
>
</ha-generic-picker>
`;
}
private _getAdditionalItems = (
searchString?: string
): PickerComboBoxItem[] => {
if (!searchString || searchString.trim() === "") {
return [];
}
const colors = this._getColors(
this.includeNone,
this.includeState,
this.defaultColor,
this.value
);
const exactMatch = colors.find((color) => color.id === searchString);
if (exactMatch) {
return [];
}
return [
{
id: searchString,
primary: this.hass.localize("ui.components.color-picker.custom_color"),
secondary: searchString,
},
];
};
private _getItems = () =>
this._getColors(
this.includeNone,
@@ -62,72 +92,70 @@ export class HaColorPicker extends LitElement {
this.value
);
private _getColors = (
includeNone: boolean,
includeState: boolean,
defaultColor: string | undefined,
currentValue: string | undefined
): PickerComboBoxItem[] => {
const items: PickerComboBoxItem[] = [];
private _getColors = memoizeOne(
(
includeNone: boolean,
includeState: boolean,
defaultColor: string | undefined,
currentValue: string | undefined
): PickerComboBoxItem[] => {
const items: PickerComboBoxItem[] = [];
const defaultSuffix = this.hass.localize(
"ui.components.color-picker.default"
);
const defaultSuffix = this.hass.localize(
"ui.components.color-picker.default"
);
const addDefaultSuffix = (label: string, isDefault: boolean) =>
isDefault && defaultSuffix ? `${label} (${defaultSuffix})` : label;
const addDefaultSuffix = (label: string, isDefault: boolean) =>
isDefault && defaultSuffix ? `${label} (${defaultSuffix})` : label;
if (includeNone) {
const noneLabel =
this.hass.localize("ui.components.color-picker.none") || "None";
items.push({
id: "none",
primary: addDefaultSuffix(noneLabel, defaultColor === "none"),
icon_path: mdiInvertColorsOff,
sorting_label: noneLabel,
if (includeNone) {
const noneLabel =
this.hass.localize("ui.components.color-picker.none") || "None";
items.push({
id: "none",
primary: addDefaultSuffix(noneLabel, defaultColor === "none"),
icon_path: mdiInvertColorsOff,
});
}
if (includeState) {
const stateLabel =
this.hass.localize("ui.components.color-picker.state") || "State";
items.push({
id: "state",
primary: addDefaultSuffix(stateLabel, defaultColor === "state"),
icon_path: mdiPalette,
});
}
Array.from(THEME_COLORS).forEach((color) => {
const themeLabel =
this.hass.localize(
`ui.components.color-picker.colors.${color}` as LocalizeKeys
) || color;
items.push({
id: color,
primary: addDefaultSuffix(themeLabel, defaultColor === color),
});
});
const isSpecial =
currentValue === "none" ||
currentValue === "state" ||
THEME_COLORS.has(currentValue || "");
const hasValue = currentValue && currentValue.length > 0;
if (hasValue && !isSpecial) {
items.push({
id: currentValue!,
primary: currentValue!,
});
}
return items;
}
if (includeState) {
const stateLabel =
this.hass.localize("ui.components.color-picker.state") || "State";
items.push({
id: "state",
primary: addDefaultSuffix(stateLabel, defaultColor === "state"),
icon_path: mdiPalette,
sorting_label: stateLabel,
});
}
Array.from(THEME_COLORS).forEach((color) => {
const themeLabel =
this.hass.localize(
`ui.components.color-picker.colors.${color}` as LocalizeKeys
) || color;
items.push({
id: color,
primary: addDefaultSuffix(themeLabel, defaultColor === color),
sorting_label: themeLabel,
});
});
const isSpecial =
currentValue === "none" ||
currentValue === "state" ||
THEME_COLORS.has(currentValue || "");
const hasValue = currentValue && currentValue.length > 0;
if (hasValue && !isSpecial) {
items.push({
id: currentValue!,
primary: currentValue!,
sorting_label: currentValue!,
});
}
return items;
};
);
private _rowRenderer: (
item: PickerComboBoxItem,
@@ -145,6 +173,9 @@ export class HaColorPicker extends LitElement {
${this._renderColorCircle(item.id)}
</span>`}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
</ha-combo-box-item>
`;

View File

@@ -14,6 +14,12 @@ import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
const SEARCH_KEYS = [
{ name: "primary", weight: 10 },
{ name: "secondary", weight: 8 },
{ name: "search_labels.english", weight: 5 },
];
export const getLanguageOptions = (
languages: string[],
nativeName: boolean,
@@ -22,6 +28,7 @@ export const getLanguageOptions = (
): PickerComboBoxItem[] => {
let options: PickerComboBoxItem[] = [];
const enLocale = { language: "en" } as FrontendLocaleData;
if (nativeName) {
const translations = translationMetadata.translations;
options = languages.map((lang) => {
@@ -37,16 +44,35 @@ export const getLanguageOptions = (
primary = lang;
}
}
const currentLang = formatLanguageCode(
lang,
locale || ({ language: navigator.language } as FrontendLocaleData)
);
const englishName = formatLanguageCode(lang, enLocale);
const secondary = currentLang !== primary ? currentLang : undefined;
return {
id: lang,
primary,
secondary,
search_labels: {
english: englishName !== primary ? englishName : null,
},
};
});
} else if (locale) {
options = languages.map((lang) => ({
id: lang,
primary: formatLanguageCode(lang, locale),
}));
options = languages.map((lang) => {
const primary = formatLanguageCode(lang, locale);
const englishName = formatLanguageCode(lang, enLocale);
return {
id: lang,
primary,
search_labels: {
english: englishName !== primary ? englishName : null,
},
};
});
}
if (!noSort && locale) {
@@ -140,6 +166,7 @@ export class HaLanguagePicker extends LitElement {
.disabled=${this.disabled}
.helper=${this.helper}
.getItems=${this._getItems}
.searchKeys=${SEARCH_KEYS}
@value-changed=${this._changed}
hide-clear-icon
>

View File

@@ -436,7 +436,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
}
const additionalItems = this._getAdditionalItems();
const additionalItems = this._getAdditionalItems(searchString);
filteredItems.push(...additionalItems);
if (this.searchFn) {

View File

@@ -21,7 +21,7 @@ export class HaTab extends LitElement {
aria-label=${ifDefined(this.name)}
@keydown=${this._handleKeyDown}
>
${this.narrow ? html`<slot name="icon"></slot>` : ""}
<slot name="icon"></slot>
<span class="name">${this.name}</span>
<ha-ripple></ha-ripple>
</div>
@@ -50,6 +50,11 @@ export class HaTab extends LitElement {
outline: none;
}
:host(:not([narrow])) div {
flex-direction: row;
gap: var(--ha-space-2);
}
.name {
white-space: nowrap;
overflow: hidden;
@@ -76,6 +81,14 @@ export class HaTab extends LitElement {
padding: 0 4px;
}
::slotted([slot="icon"]) {
margin-bottom: var(--ha-space-1);
}
:host(:not([narrow])) ::slotted([slot="icon"]) {
margin-bottom: 0;
}
div:focus-visible:before {
position: absolute;
display: block;

View File

@@ -13,6 +13,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import { isIosApp } from "../util/is_ios";
import "./ha-dialog-header";
import "./ha-icon-button";
@@ -184,6 +185,21 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
await this.updateComplete;
requestAnimationFrame(() => {
if (isIosApp(this.hass)) {
const element = this.querySelector("[autofocus]");
if (element !== null) {
if (!element.id) {
element.id = "ha-wa-dialog-autofocus";
}
this.hass.auth.external!.fireMessage({
type: "focus_element",
payload: {
element_id: element.id,
},
});
}
return;
}
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
});
};

View File

@@ -96,6 +96,22 @@ export interface ConfigEntryUpdate {
entry: ConfigEntry;
}
export const subscribeAndProcessConfigEntries = (
hass: HomeAssistant,
callbackFunction: (entries: ConfigEntry[]) => void,
filters?: {
type?: IntegrationType[];
domain?: string;
}
): Promise<UnsubscribeFunc> => {
const stream = new ConfigEntryStream();
const processCallback = (messages: ConfigEntryUpdate[]) => {
callbackFunction(stream.processMessage(messages));
};
return subscribeConfigEntries(hass, processCallback, filters);
};
export const subscribeConfigEntries = (
hass: HomeAssistant,
callbackFunction: (message: ConfigEntryUpdate[]) => void,
@@ -110,10 +126,9 @@ export const subscribeConfigEntries = (
if (filters && filters.type) {
params.type_filter = filters.type;
}
return hass.connection.subscribeMessage<ConfigEntryUpdate[]>(
(message) => callbackFunction(message),
params
);
return hass.connection.subscribeMessage<ConfigEntryUpdate[]>((message) => {
callbackFunction(message);
}, params);
};
export const getConfigEntries = (
@@ -206,3 +221,29 @@ export const sortConfigEntries = (
);
return [primaryEntry, ...otherEntries];
};
export class ConfigEntryStream {
private _entries: ConfigEntry[] = [];
processMessage(message: ConfigEntryUpdate[]) {
message.forEach((configEntry) => {
if (configEntry.type === null || configEntry.type === "added") {
this._entries.push(configEntry.entry);
return;
}
if (configEntry.type === "removed") {
this._entries = this._entries.filter(
(entry) => entry.entry_id !== configEntry.entry.entry_id
);
return;
}
if (configEntry.type === "updated") {
const newEntry = configEntry.entry;
this._entries = this._entries.map((entry) =>
entry.entry_id === newEntry.entry_id ? newEntry : entry
);
}
});
return this._entries;
}
}

View File

@@ -1,6 +1,7 @@
import { createContext } from "@lit/context";
import type { HassConfig } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import type { ConfigEntry } from "./config_entries";
import type { EntityRegistryEntry } from "./entity/entity_registry";
import type { LabelRegistryEntry } from "./label/label_registry";
@@ -30,3 +31,6 @@ export const fullEntitiesContext =
export const floorsContext = createContext<HomeAssistant["floors"]>("floors");
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
export const configEntriesContext =
createContext<ConfigEntry[]>("configEntries");

View File

@@ -1,10 +1,12 @@
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { atLeastVersion } from "../common/config/version";
import type { HomeAssistant } from "../types";
import type { HassioAddonInfo } from "./hassio/addon";
export interface LogProvider {
key: string;
name: string;
addon?: HassioAddonInfo;
}
export const fetchErrorLog = (hass: HomeAssistant) =>

View File

@@ -1,5 +1,6 @@
import { ContextProvider } from "@lit/context";
import { mdiContentSave, mdiHelpCircle } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { load } from "js-yaml";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
@@ -46,7 +47,13 @@ import {
isTrigger,
normalizeAutomationConfig,
} from "../../../data/automation";
import {
subscribeAndProcessConfigEntries,
type ConfigEntry,
} from "../../../data/config_entries";
import { configEntriesContext } from "../../../data/context";
import { getActionType, type Action } from "../../../data/script";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
@@ -80,7 +87,7 @@ const automationConfigStruct = union([
export const SIDEBAR_DEFAULT_WIDTH = 500;
@customElement("manual-automation-editor")
export class HaManualAutomationEditor extends LitElement {
export class HaManualAutomationEditor extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@@ -117,6 +124,11 @@ export class HaManualAutomationEditor extends LitElement {
HaAutomationAction | HaAutomationCondition
>;
private _configEntries = new ContextProvider(this, {
context: configEntriesContext,
initialValue: [],
});
private _prevSidebarWidthPx?: number;
public connectedCallback() {
@@ -124,6 +136,18 @@ export class HaManualAutomationEditor extends LitElement {
window.addEventListener("paste", this._handlePaste);
}
public hassSubscribe(): Promise<UnsubscribeFunc>[] {
return [
subscribeAndProcessConfigEntries(
this.hass,
(message: ConfigEntry[]) => {
this._configEntries.setValue(message);
},
undefined
),
];
}
public disconnectedCallback() {
window.removeEventListener("paste", this._handlePaste);
super.disconnectedCallback();

View File

@@ -1,17 +1,15 @@
import { consume } from "@lit/context";
import { mdiAlert, mdiFormatListBulleted, mdiShape } from "@mdi/js";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { LitElement, css, html, nothing, type TemplateResult } from "lit";
import { css, html, LitElement, type nothing, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import { ensureArray } from "../../../../common/array/ensure-array";
import { transform } from "../../../../common/decorators/transform";
import "../../../../components/ha-svg-icon";
import {
getConfigEntries,
type ConfigEntry,
} from "../../../../data/config_entries";
import type { ConfigEntry } from "../../../../data/config_entries";
import {
areasContext,
configEntriesContext,
devicesContext,
floorsContext,
labelsContext,
@@ -55,6 +53,13 @@ export class HaAutomationRowTargets extends LitElement {
@consume({ context: labelsContext, subscribe: true })
private _labelRegistry!: LabelRegistryEntry[];
@state()
@consume({ context: configEntriesContext, subscribe: true })
@transform<ConfigEntry[], Record<string, ConfigEntry>>({
transformer: function (value) {
return Object.fromEntries(value.map((entry) => [entry.entry_id, entry]));
},
})
private _configEntryLookup?: Record<string, ConfigEntry>;
protected render() {
@@ -149,13 +154,6 @@ export class HaAutomationRowTargets extends LitElement {
</div>`;
}
private async _loadConfigEntries() {
const configEntries = await getConfigEntries(this.hass);
this._configEntryLookup = Object.fromEntries(
configEntries.map((entry) => [entry.entry_id, entry])
);
}
private _renderTarget(
targetType: "floor" | "area" | "device" | "entity" | "label",
targetId: string
@@ -178,22 +176,6 @@ export class HaAutomationRowTargets extends LitElement {
);
}
if (targetType === "device" && !this._configEntryLookup) {
const loadConfigEntries = this._loadConfigEntries().then(() =>
this._renderTargetBadge(
getTargetIcon(
this.hass,
targetType,
targetId,
this._configEntryLookup!
),
getTargetText(this.hass, targetType, targetId)
)
);
return html`${until(loadConfigEntries, nothing)}`;
}
return this._renderTargetBadge(
getTargetIcon(
this.hass,

View File

@@ -1,20 +1,31 @@
import { mdiChevronDown } from "@mdi/js";
import {
mdiChevronDown,
mdiChip,
mdiDns,
mdiPackageVariant,
mdiPuzzle,
mdiRadar,
mdiVolumeHigh,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { atLeastVersion } from "../../../common/config/version";
import { navigate } from "../../../common/navigate";
import { stringCompare } from "../../../common/string/compare";
import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-generic-picker";
import type { HaGenericPicker } from "../../../components/ha-generic-picker";
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
import "../../../components/search-input";
import type { LogProvider } from "../../../data/error_log";
import { fetchHassioAddonsInfo } from "../../../data/hassio/addon";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-subpage";
import { mdiHomeAssistant } from "../../../resources/home-assistant-logo-svg";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
import "./error-log-card";
@@ -64,6 +75,8 @@ export class HaConfigLogs extends LitElement {
@query("system-log-card") private systemLog?: SystemLogCard;
@query("ha-generic-picker") private providerPicker?: HaGenericPicker;
@state() private _selectedLogProvider = "core";
@state() private _logProviders = logProviders;
@@ -109,6 +122,8 @@ export class HaConfigLogs extends LitElement {
</div>
`;
const selectedProvider = this._getActiveProvider(this._selectedLogProvider);
return html`
<hass-subpage
.hass=${this.hass}
@@ -116,33 +131,39 @@ export class HaConfigLogs extends LitElement {
.header=${this.hass.localize("ui.panel.config.logs.caption")}
back-path="/config/system"
>
${isComponentLoaded(this.hass, "hassio")
${isComponentLoaded(this.hass, "hassio") && this._logProviders
? html`
<ha-dropdown
<ha-generic-picker
slot="toolbar-icon"
@wa-select=${this._handleDropdownSelect}
.hass=${this.hass}
.getItems=${this._getLogProviderItems}
value=""
.rowRenderer=${this._providerRenderer}
@value-changed=${this._handleDropdownSelect}
>
<ha-button slot="trigger" appearance="filled">
<ha-button
slot="field"
appearance="filled"
@click=${this._openPicker}
>
${selectedProvider?.icon
? html`<img
src=${selectedProvider.icon}
alt=${selectedProvider.primary}
slot="start"
/>`
: selectedProvider?.icon_path
? html`<ha-svg-icon
slot="start"
.path=${selectedProvider.icon_path}
></ha-svg-icon>`
: nothing}
${selectedProvider?.primary}
<ha-svg-icon slot="end" .path=${mdiChevronDown}></ha-svg-icon>
${this._logProviders.find(
(p) => p.key === this._selectedLogProvider
)!.name}
</ha-button>
${this._logProviders.map(
(provider) => html`
<ha-dropdown-item
.value=${provider.key}
class=${provider.key === this._selectedLogProvider
? "selected"
: ""}
>
${provider.name}
</ha-dropdown-item>
`
)}
</ha-dropdown>
</ha-generic-picker>
`
: ""}
: nothing}
${search}
<div class="content">
${this._selectedLogProvider === "core" && !this._detail
@@ -175,8 +196,13 @@ export class HaConfigLogs extends LitElement {
this._detail = !this._detail;
}
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
const provider = ev.detail?.item?.value;
private _openPicker(ev: Event) {
ev.stopPropagation();
this.providerPicker?.open();
}
private _handleDropdownSelect(ev: CustomEvent<{ value: string }>) {
const provider = ev.detail?.value;
if (!provider) {
return;
}
@@ -223,6 +249,7 @@ export class HaConfigLogs extends LitElement {
.map((addon) => ({
key: addon.slug,
name: addon.name,
addon,
}))
.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
@@ -234,6 +261,78 @@ export class HaConfigLogs extends LitElement {
}
}
private _getLogProviderItems = (): PickerComboBoxItem[] =>
this._logProviders.map((provider) => ({
id: provider.key,
primary: provider.name,
icon: provider.addon
? atLeastVersion(this.hass.config.version, 0, 105) &&
provider.addon.icon
? `/api/hassio/addons/${provider.addon.slug}/icon`
: undefined
: undefined,
icon_path: provider.addon
? mdiPuzzle
: this._getProviderIconPath(provider.key),
}));
private _providerRenderer = (item: PickerComboBoxItem) => html`
<ha-combo-box-item type="button" compact>
${item.icon
? html`<img src=${item.icon} alt=${item.primary} slot="start" />`
: item.icon_path
? html`<ha-svg-icon
slot="start"
.path=${item.icon_path}
></ha-svg-icon>`
: nothing}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
</ha-combo-box-item>
`;
private _getActiveProvider = memoizeOne((selectedLogProvider: string) => {
const provider = this._logProviders.find(
(p) => p.key === selectedLogProvider
);
if (provider) {
return {
id: provider.key,
primary: provider.name,
icon: provider.addon
? atLeastVersion(this.hass.config.version, 0, 105) &&
provider.addon.icon
? `/api/hassio/addons/${provider.addon.slug}/icon`
: undefined
: undefined,
icon_path: provider.addon
? mdiPuzzle
: this._getProviderIconPath(provider.key),
};
}
return undefined;
});
private _getProviderIconPath(providerKey: string): string | undefined {
switch (providerKey) {
case "core":
return mdiHomeAssistant;
case "supervisor":
return mdiPackageVariant;
case "host":
return mdiChip;
case "dns":
return mdiDns;
case "audio":
return mdiVolumeHigh;
case "multicast":
return mdiRadar;
}
return undefined;
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -262,8 +361,17 @@ export class HaConfigLogs extends LitElement {
.content {
direction: ltr;
}
ha-generic-picker {
--md-list-item-leading-icon-color: var(--ha-color-primary-50);
--mdc-icon-size: 32px;
}
img {
height: 32px;
}
@media all and (max-width: 870px) {
ha-dropdown {
ha-generic-picker {
max-width: 50%;
}
ha-button {
@@ -274,9 +382,6 @@ export class HaConfigLogs extends LitElement {
white-space: nowrap;
}
}
ha-dropdown-item.selected {
font-weight: var(--ha-font-weight-bold);
}
`,
];
}

View File

@@ -1,4 +1,6 @@
import { ContextProvider } from "@lit/context";
import { mdiContentSave, mdiHelpCircle } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { load } from "js-yaml";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
@@ -35,6 +37,8 @@ import type {
ActionSidebarConfig,
SidebarConfig,
} from "../../../data/automation";
import { subscribeAndProcessConfigEntries } from "../../../data/config_entries";
import { configEntriesContext } from "../../../data/context";
import type {
Action,
Fields,
@@ -46,6 +50,7 @@ import {
MODES,
normalizeScriptConfig,
} from "../../../data/script";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
@@ -70,7 +75,7 @@ const scriptConfigStruct = object({
});
@customElement("manual-script-editor")
export class HaManualScriptEditor extends LitElement {
export class HaManualScriptEditor extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@@ -108,6 +113,11 @@ export class HaManualScriptEditor extends LitElement {
HaAutomationAction | HaScriptFields
>;
private _configEntries = new ContextProvider(this, {
context: configEntriesContext,
initialValue: [],
});
private _openFields = false;
private _prevSidebarWidthPx?: number;
@@ -129,6 +139,14 @@ export class HaManualScriptEditor extends LitElement {
});
}
public hassSubscribe(): Promise<UnsubscribeFunc>[] {
return [
subscribeAndProcessConfigEntries(this.hass, (configEntries) => {
this._configEntries.setValue(configEntries);
}),
];
}
protected updated(changedProps) {
if (this._openFields && changedProps.has("config")) {
this._openFields = false;

View File

@@ -52,6 +52,8 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
"home-media-players": () =>
import("./home/home-media-players-view-strategy"),
"home-area": () => import("./home/home-area-view-strategy"),
"home-other-devices": () =>
import("./home/home-other-devices-view-strategy"),
light: () => import("../../light/strategies/light-view-strategy"),
security: () => import("../../security/strategies/security-view-strategy"),
climate: () => import("../../climate/strategies/climate-view-strategy"),

View File

@@ -0,0 +1,33 @@
import { ASSIST_ENTITIES } from "../../../../../common/const";
import type { EntityFilter } from "../../../../../common/entity/entity_filter";
export const OTHER_DEVICES_FILTERS: EntityFilter[] = [
{
area: null,
hidden_platform: [
"automation",
"script",
"hassio",
"backup",
"mobile_app",
"zone",
"person",
],
hidden_domains: [
"ai_task",
"automation",
"configurator",
"device_tracker",
"event",
"geo_location",
"notify",
"persistent_notification",
"script",
"sun",
"tag",
"todo",
"zone",
...ASSIST_ENTITIES,
],
},
];

View File

@@ -71,6 +71,16 @@ export class HomeDashboardStrategy extends ReactiveElement {
icon: HOME_SUMMARIES_ICONS.media_players,
} satisfies LovelaceViewRawConfig;
const otherDevicesView = {
title: hass.localize("ui.panel.lovelace.strategy.home.devices"),
path: "other-devices",
subview: true,
strategy: {
type: "home-other-devices",
},
icon: "mdi:devices",
} satisfies LovelaceViewRawConfig;
return {
views: [
{
@@ -83,6 +93,7 @@ export class HomeDashboardStrategy extends ReactiveElement {
},
...areaViews,
mediaPlayersView,
otherDevicesView,
],
};
}

View File

@@ -0,0 +1,208 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
import {
findEntities,
generateEntityFilter,
} from "../../../../common/entity/entity_filter";
import { clamp } from "../../../../common/number/clamp";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import { isHelperDomain } from "../../../config/helpers/const";
import type { HeadingCardConfig } from "../../cards/types";
import { OTHER_DEVICES_FILTERS } from "./helpers/other-devices-filters";
export interface HomeOtherDevicesViewStrategyConfig {
type: "home-other-devices";
}
@customElement("home-other-devices-view-strategy")
export class HomeOtherDevicesViewStrategy extends ReactiveElement {
static async generate(
_config: HomeOtherDevicesViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const allEntities = Object.keys(hass.states);
const otherDevicesFilters = OTHER_DEVICES_FILTERS.map((filter) =>
generateEntityFilter(hass, filter)
);
const otherDevicesEntities = findEntities(allEntities, otherDevicesFilters);
const sections: LovelaceSectionRawConfig[] = [];
const entitiesByDevice: Record<string, string[]> = {};
const entitiesWithoutDevices: string[] = [];
for (const entityId of otherDevicesEntities) {
const stateObj = hass.states[entityId];
if (!stateObj) continue;
const { device } = getEntityContext(
stateObj,
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
if (!device) {
entitiesWithoutDevices.push(entityId);
continue;
}
if (!(device.id in entitiesByDevice)) {
entitiesByDevice[device.id] = [];
}
entitiesByDevice[device.id].push(entityId);
}
const devicesEntities = Object.entries(entitiesByDevice).map(
([deviceId, entities]) => ({
device_id: deviceId,
entities: entities,
})
);
const helpersEntities = entitiesWithoutDevices.filter((entityId) => {
const domain = computeDomain(entityId);
return isHelperDomain(domain);
});
const otherEntities = entitiesWithoutDevices.filter((entityId) => {
const domain = computeDomain(entityId);
return !isHelperDomain(domain);
});
const batteryFilter = generateEntityFilter(hass, {
domain: "sensor",
device_class: "battery",
});
const energyFilter = generateEntityFilter(hass, {
domain: "sensor",
device_class: ["energy", "power"],
});
const primaryFilter = generateEntityFilter(hass, {
entity_category: "none",
});
for (const deviceEntities of devicesEntities) {
if (deviceEntities.entities.length === 0) continue;
const batteryEntities = deviceEntities.entities.filter((e) =>
batteryFilter(e)
);
const entities = deviceEntities.entities.filter(
(e) => !batteryFilter(e) && !energyFilter(e) && primaryFilter(e)
);
if (entities.length === 0) {
continue;
}
const deviceId = deviceEntities.device_id;
const device = hass.devices[deviceId];
let heading = "";
if (device) {
heading =
computeDeviceName(device) ||
hass.localize("ui.panel.lovelace.strategy.home.unamed_device");
}
sections.push({
type: "grid",
cards: [
{
type: "heading",
heading: heading,
tap_action: device
? {
action: "navigate",
navigation_path: `/config/devices/device/${device.id}`,
}
: undefined,
badges: [
...batteryEntities.slice(0, 1).map((e) => ({
entity: e,
type: "entity",
tap_action: {
action: "more-info",
},
})),
],
} satisfies HeadingCardConfig,
...entities.map((e) => ({
type: "tile",
entity: e,
name: {
type: "entity",
},
})),
],
});
}
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
const maxColumns = clamp(sections.length, 2, 3);
if (helpersEntities.length) {
sections.push({
type: "grid",
column_span: maxColumns,
cards: [
{
type: "heading",
heading: hass.localize(
"ui.panel.lovelace.strategy.other_devices.helpers"
),
} satisfies HeadingCardConfig,
...helpersEntities.map((e) => ({
type: "tile",
entity: e,
})),
],
});
}
if (otherEntities.length) {
sections.push({
type: "grid",
column_span: maxColumns,
cards: [
{
type: "heading",
heading: hass.localize(
"ui.panel.lovelace.strategy.other_devices.entities"
),
} satisfies HeadingCardConfig,
...otherEntities.map((e) => ({
type: "tile",
entity: e,
})),
],
});
}
// Take the full width if there is only one section to avoid narrow header on desktop
if (sections.length === 1) {
sections[0].column_span = 2;
}
return {
type: "sections",
header: {
badges_position: "bottom",
},
max_columns: maxColumns,
sections: sections,
};
}
}
declare global {
interface HTMLElementTagNameMap {
"home-other-devices-view-strategy": HomeOtherDevicesViewStrategy;
}
}

View File

@@ -22,10 +22,12 @@ import type {
HomeSummaryCard,
MarkdownCardConfig,
TileCardConfig,
WeatherForecastCardConfig,
} from "../../cards/types";
import type { Condition } from "../../common/validate-condition";
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
import type { Condition } from "../../common/validate-condition";
import { OTHER_DEVICES_FILTERS } from "./helpers/other-devices-filters";
export interface HomeOverviewViewStrategyConfig {
type: "home-overview";
@@ -73,16 +75,25 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
const maxColumns = 3;
const allEntities = Object.keys(hass.states);
const largeScreenCondition: Condition = {
condition: "screen",
media_query: "(min-width: 871px)",
};
const smallScreenCondition: Condition = {
condition: "screen",
media_query: "(max-width: 870px)",
};
const otherDevicesFilters = OTHER_DEVICES_FILTERS.map((filter) =>
generateEntityFilter(hass, filter)
);
const entitiesWithoutAreas = findEntities(allEntities, otherDevicesFilters);
const floorsSections: LovelaceSectionConfig[] = [];
for (const floorStructure of home.floors) {
const floorId = floorStructure.id;
@@ -114,23 +125,57 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
}
}
if (home.areas.length) {
if (home.areas.length > 0 || entitiesWithoutAreas.length > 0) {
const cards: LovelaceCardConfig[] = [];
for (const areaId of home.areas) {
cards.push(computeAreaCard(areaId, hass));
}
if (entitiesWithoutAreas.length > 0) {
cards.push({
type: "tile",
entity: "zone.home", // zone entity to represent unassigned area as it always exists
vertical: true,
name: hass.localize("ui.panel.lovelace.strategy.home.devices"),
icon: "mdi:devices",
hide_state: true,
tap_action: {
action: "navigate",
navigation_path: "other-devices",
},
grid_options: {
rows: 2,
columns: 4,
},
} as TileCardConfig);
}
const noOtherAreas = home.areas.length === 0;
const noFloor = home.floors.length === 0;
// Other areas / Areas / Others / nothing
const heading =
noFloor && noOtherAreas
? undefined
: noFloor
? hass.localize("ui.panel.lovelace.strategy.home.areas")
: noOtherAreas
? hass.localize("ui.panel.lovelace.strategy.home.devices")
: hass.localize("ui.panel.lovelace.strategy.home.other_areas");
floorsSections.push({
type: "grid",
column_span: maxColumns,
cards: [
{
type: "heading",
heading:
floorCount > 1
? hass.localize("ui.panel.lovelace.strategy.home.other_areas")
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
heading_style: "title",
},
...(heading
? [
{
type: "heading",
heading: heading,
heading_style: "title",
},
]
: []),
...cards,
],
});
@@ -141,19 +186,18 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
);
const maxCommonControls = Math.max(8, favoriteEntities.length);
const commonControlsSection = {
const favoritesSection = {
strategy: {
type: "common-controls",
limit: maxCommonControls,
include_entities: favoriteEntities,
title: hass.localize("ui.panel.lovelace.strategy.home.favorites"),
title_visibilty: [largeScreenCondition],
hide_empty: true,
} satisfies CommonControlSectionStrategyConfig,
column_span: maxColumns,
} as LovelaceStrategySectionConfig;
const allEntities = Object.keys(hass.states);
const mediaPlayerFilter = HOME_SUMMARIES_FILTERS.media_players.map(
(filter) => generateEntityFilter(hass, filter)
);
@@ -260,10 +304,18 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
}));
// Build summary cards for mobile section (half width: columns 6)
const mobileSummaryCards = summaryCards.map((card) => ({
...card,
grid_options: { columns: 6 },
}));
const mobileSummaryCards = [
...summaryCards.map((card) => ({
...card,
grid_options: { columns: 6 },
})),
];
const summaryHeadingCard: LovelaceCardConfig = {
type: "heading",
heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"),
heading_style: "title",
};
// Mobile summary section (visible on small screens only)
const mobileSummarySection: LovelaceSectionConfig | undefined =
@@ -272,7 +324,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
type: "grid",
column_span: maxColumns,
visibility: [smallScreenCondition],
cards: mobileSummaryCards,
cards: [summaryHeadingCard, ...mobileSummaryCards],
}
: undefined;
@@ -281,25 +333,15 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
sidebarSummaryCards.length > 0
? {
type: "grid",
cards: [
{
type: "heading",
heading: hass.localize(
"ui.panel.lovelace.strategy.home.for_you"
),
heading_style: "title",
},
...sidebarSummaryCards,
],
cards: [summaryHeadingCard, ...sidebarSummaryCards],
}
: undefined;
const sections = (
[
mobileSummarySection,
commonControlsSection,
...floorsSections,
] satisfies (LovelaceSectionRawConfig | undefined)[]
[favoritesSection, mobileSummarySection, ...floorsSections] satisfies (
| LovelaceSectionRawConfig
| undefined
)[]
).filter(Boolean) as LovelaceSectionRawConfig[];
return {
@@ -319,7 +361,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
sections: [sidebarSection],
content_label: hass.localize("ui.panel.lovelace.strategy.home.home"),
sidebar_label: hass.localize(
"ui.panel.lovelace.strategy.home.for_you"
"ui.panel.lovelace.strategy.home.summaries"
),
visibility: [largeScreenCondition],
},

View File

@@ -5,6 +5,7 @@ import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/sec
import { getCommonControlUsagePrediction } from "../../../../data/usage_prediction";
import type { HomeAssistant } from "../../../../types";
import type { HeadingCardConfig, TileCardConfig } from "../../cards/types";
import type { Condition } from "../../common/validate-condition";
const DEFAULT_LIMIT = 8;
@@ -16,6 +17,7 @@ export interface CommonControlSectionStrategyConfig {
exclude_entities?: string[];
include_entities?: string[];
hide_empty?: boolean;
title_visibilty?: Condition[];
}
@customElement("common-controls-section-strategy")
@@ -34,6 +36,7 @@ export class CommonControlsSectionStrategy extends ReactiveElement {
type: "heading",
heading: config.title,
icon: config.icon,
visibility: config.title_visibilty,
} satisfies HeadingCardConfig);
}

View File

@@ -921,9 +921,13 @@
"default": "default",
"state": "State color",
"none": "No color",
"custom_color": "Custom color",
"no_colors_found": "No matching colors found",
"colors": {
"primary": "Primary",
"accent": "Accent",
"primary-text": "Main text",
"secondary-text": "Secondary text",
"disabled": "Disabled",
"inactive": "Inactive",
"red": "Red",
@@ -7173,6 +7177,7 @@
"home": {
"summary_list": {
"media_players": "Media players",
"other_devices": "Other devices"
"weather": "Weather",
"energy": "Today's energy"
},
@@ -7180,6 +7185,7 @@
"summaries": "Summaries",
"areas": "Areas",
"other_areas": "Other areas",
"devices": "Devices",
"unamed_device": "Unnamed device",
"others": "Others",
"scenes": "Scenes",
@@ -7207,6 +7213,10 @@
"home_media_players": {
"media_players": "Media players",
"other_media_players": "Other media players"
},
"other_devices": {
"helpers": "Helpers",
"entities": "Entities"
}
},
"cards": {

208
yarn.lock
View File

@@ -1696,15 +1696,15 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/ecma402-abstract@npm:3.0.2":
version: 3.0.2
resolution: "@formatjs/ecma402-abstract@npm:3.0.2"
"@formatjs/ecma402-abstract@npm:3.0.3":
version: 3.0.3
resolution: "@formatjs/ecma402-abstract@npm:3.0.3"
dependencies:
"@formatjs/fast-memoize": "npm:3.0.1"
"@formatjs/intl-localematcher": "npm:0.7.2"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/a0e5838b141189331bdf63d36b101b98e3f78465813acf41d7a42c7f0733ba4e72815a7d5258bb7b97fcd455e0721ff14fc8e9478b33385f73ef460ff6b33d22
checksum: 10/134192d5ee45e215b3b61d06849f4f311948629417cd57aa4eb26bd8ca8c13d39660b2d3d46797a83f811c877a86d691ca58969fe889be59241782a59b638db1
languageName: node
linkType: hard
@@ -1717,100 +1717,100 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/icu-messageformat-parser@npm:3.0.2":
version: 3.0.2
resolution: "@formatjs/icu-messageformat-parser@npm:3.0.2"
"@formatjs/icu-messageformat-parser@npm:3.0.4":
version: 3.0.4
resolution: "@formatjs/icu-messageformat-parser@npm:3.0.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.2"
"@formatjs/icu-skeleton-parser": "npm:2.0.2"
"@formatjs/ecma402-abstract": "npm:3.0.3"
"@formatjs/icu-skeleton-parser": "npm:2.0.3"
tslib: "npm:^2.8.0"
checksum: 10/457d04de7978f3b7a5504cad53bf61a79e6dcce482261699162233e006726dc22557681a676d449297962c511cb102b1cc4cfd9d3710135cbda7f8b57bd8f630
checksum: 10/87bc0504a07b536a17f2ef7fe61529352b67b42f7e9253441120bcf105958747315031a15939791487358198d8790a85cb112c956216bbe07d6e2f284a78279e
languageName: node
linkType: hard
"@formatjs/icu-skeleton-parser@npm:2.0.2":
version: 2.0.2
resolution: "@formatjs/icu-skeleton-parser@npm:2.0.2"
"@formatjs/icu-skeleton-parser@npm:2.0.3":
version: 2.0.3
resolution: "@formatjs/icu-skeleton-parser@npm:2.0.3"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.2"
"@formatjs/ecma402-abstract": "npm:3.0.3"
tslib: "npm:^2.8.0"
checksum: 10/d153fe022ca7ac11e216dfac3297e20387269711689b5c2da8c1f046883e7805bbfd0e76fdf54ac1346a7d6c7283eb455c0d7a23e8c90b8d2db7a1bb0da846ce
checksum: 10/a402b4cc6765a718ad2b83bb97becc0e17b5cf400d6216e629f1804bd224dbf3a8c04f4a0e3fc5ab4dcf1b449af975d750e003e7958a0eaae8db87b47ac2f971
languageName: node
linkType: hard
"@formatjs/intl-datetimeformat@npm:7.0.2":
version: 7.0.2
resolution: "@formatjs/intl-datetimeformat@npm:7.0.2"
"@formatjs/intl-datetimeformat@npm:7.0.4":
version: 7.0.4
resolution: "@formatjs/intl-datetimeformat@npm:7.0.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.2"
"@formatjs/ecma402-abstract": "npm:3.0.3"
"@formatjs/intl-localematcher": "npm:0.7.2"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/7fc86a7d3846ffb73324a75f4cb59e50d5ee0d741db2e211a534c480b70939e92f368a4ed388562924d32abaed4786a33d88be98f2c361f0d5855f10615059e9
checksum: 10/f3f5a4de0f6a2e39ba476ab84f9b2c3a2d8dc9e5536659af08db58e6e450bd79eb49d6a9a83df3a83b701953455add0293dbc2f4e5a09793b8d4c0c1ceb63788
languageName: node
linkType: hard
"@formatjs/intl-displaynames@npm:7.0.2":
version: 7.0.2
resolution: "@formatjs/intl-displaynames@npm:7.0.2"
"@formatjs/intl-displaynames@npm:7.0.4":
version: 7.0.4
resolution: "@formatjs/intl-displaynames@npm:7.0.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.2"
"@formatjs/ecma402-abstract": "npm:3.0.3"
"@formatjs/intl-localematcher": "npm:0.7.2"
tslib: "npm:^2.8.0"
checksum: 10/ca017e9d09d7fa8c2c783cc76ff7aa0614e32fb937d7eb262610ab76fced4d9c7cbc26f62af259936ac17e42e755367282b86a2a9f396bab293713b5fefe41ed
checksum: 10/2273cb9b4a4ef66b14cdc8dfe6707bb4b58e75cd98de3a1998143d3302e164f40238a0e20df382e9fc4d496063f8d605f41829e68e2d8da56106b252f28053a3
languageName: node
linkType: hard
"@formatjs/intl-durationformat@npm:0.8.2":
version: 0.8.2
resolution: "@formatjs/intl-durationformat@npm:0.8.2"
"@formatjs/intl-durationformat@npm:0.8.4":
version: 0.8.4
resolution: "@formatjs/intl-durationformat@npm:0.8.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.2"
"@formatjs/ecma402-abstract": "npm:3.0.3"
"@formatjs/intl-localematcher": "npm:0.7.2"
tslib: "npm:^2.8.0"
checksum: 10/7f16359e6a56dc9d14a4595557b8fdd4304722e876fa19cf423bfa931232c3d808111b401fa9c45363477d026dada9cb9b22271f2abc7829b84d6ff9b0fb78a4
checksum: 10/1eca9dfacf931ce6ca4c1c2e4d7c20f145a09615aa0e530152524645e3f4d31e2238416cdac57f2ea80534f14013193aa070d865dc91cdd2fb5cf191bbe40ac3
languageName: node
linkType: hard
"@formatjs/intl-enumerator@npm:2.0.2":
version: 2.0.2
resolution: "@formatjs/intl-enumerator@npm:2.0.2"
"@formatjs/intl-enumerator@npm:2.0.3":
version: 2.0.3
resolution: "@formatjs/intl-enumerator@npm:2.0.3"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.2"
"@formatjs/ecma402-abstract": "npm:3.0.3"
tslib: "npm:^2.8.0"
checksum: 10/9ad06c1107b7134ab572b1c8a4bcf70644966b04da32045480f46ba2b05c5d1e5282d7da0b15485685644dea56e3954a38238c4f96656c4efc161ccf701d4ff2
checksum: 10/6ab5d123a2b66cd84af8143fe2c7bafcd2492757874b36cb01a78a1810c5de085e103e590b619c3425c6f57e97c968e68334cf95c114f9d7d17766cda7fe56f3
languageName: node
linkType: hard
"@formatjs/intl-getcanonicallocales@npm:3.0.2":
version: 3.0.2
resolution: "@formatjs/intl-getcanonicallocales@npm:3.0.2"
"@formatjs/intl-getcanonicallocales@npm:3.0.3":
version: 3.0.3
resolution: "@formatjs/intl-getcanonicallocales@npm:3.0.3"
dependencies:
tslib: "npm:^2.8.0"
checksum: 10/1b14f44d7912d27b28f3ea4658040709297a858fbfc6e23a712edccab7aab50a6c243d447a608fd3f8466d146b59ec9c14e1ed9bec090ed79ef230b6cf3a707a
checksum: 10/c69c976e742124943831fb196fb5d428bfd6ba74a6fd112edba88f776b258b76c8092180e7f2729608d3b2b588c833c7f1684a669322a2aad8de39992d8faa34
languageName: node
linkType: hard
"@formatjs/intl-listformat@npm:8.0.2":
version: 8.0.2
resolution: "@formatjs/intl-listformat@npm:8.0.2"
"@formatjs/intl-listformat@npm:8.0.4":
version: 8.0.4
resolution: "@formatjs/intl-listformat@npm:8.0.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.2"
"@formatjs/ecma402-abstract": "npm:3.0.3"
"@formatjs/intl-localematcher": "npm:0.7.2"
tslib: "npm:^2.8.0"
checksum: 10/982839fa2e9650e53dee03693789e22a64846fa43ac30c263d4121cbfb3591bc7b2f5a13cb412283016bee3f06047238ef4d8bcf0d4d929b540e1e5dfd90ab40
checksum: 10/2bc17b9315a30afc1f667ad35475daaf35430093da30280ff531944e088b184085b1cea9e77bb040b7f668d1b4b47d4616c5e1792a097858aa9a436127875c1f
languageName: node
linkType: hard
"@formatjs/intl-locale@npm:5.0.2":
version: 5.0.2
resolution: "@formatjs/intl-locale@npm:5.0.2"
"@formatjs/intl-locale@npm:5.0.4":
version: 5.0.4
resolution: "@formatjs/intl-locale@npm:5.0.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.2"
"@formatjs/intl-enumerator": "npm:2.0.2"
"@formatjs/intl-getcanonicallocales": "npm:3.0.2"
"@formatjs/ecma402-abstract": "npm:3.0.3"
"@formatjs/intl-enumerator": "npm:2.0.3"
"@formatjs/intl-getcanonicallocales": "npm:3.0.3"
tslib: "npm:^2.8.0"
checksum: 10/cba579c5a5600bd577433da2ee39a772d55e39de065992c54fb2f54ccb712263d7fdf1ae360e7ac31aea2b39794daca32fec7496d9b2e48f4bf6e19b3bd87c8e
checksum: 10/e0c32e6718052af6c05c5e08249ac320fc6e8b336f3d61332b1bf8987c9157b0d7e5ee51546a5754b2c7799a65ec25da0a5d9d24798bc64ad58fa9aced1e90d8
languageName: node
linkType: hard
@@ -1823,38 +1823,38 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/intl-numberformat@npm:9.0.3":
version: 9.0.3
resolution: "@formatjs/intl-numberformat@npm:9.0.3"
"@formatjs/intl-numberformat@npm:9.0.5":
version: 9.0.5
resolution: "@formatjs/intl-numberformat@npm:9.0.5"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.2"
"@formatjs/ecma402-abstract": "npm:3.0.3"
"@formatjs/intl-localematcher": "npm:0.7.2"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/3bc32f76a32974ad51a7b23f8b9a5301f44550f4abfb6ffa3a84a0ad492072a32ab97b1e2dba130cb4a2290640d444dacdaeb8afaad6126bc542d202de22499d
checksum: 10/72c6eaae12e270aad4d395fd36e6cfc7201452dd264bdbffbcecddbec34d3e2bfd86b0bd08e4ba04d771b9d1de9956554687405f4daa509e1c560f741aabf399
languageName: node
linkType: hard
"@formatjs/intl-pluralrules@npm:6.0.2":
version: 6.0.2
resolution: "@formatjs/intl-pluralrules@npm:6.0.2"
"@formatjs/intl-pluralrules@npm:6.0.4":
version: 6.0.4
resolution: "@formatjs/intl-pluralrules@npm:6.0.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.2"
"@formatjs/ecma402-abstract": "npm:3.0.3"
"@formatjs/intl-localematcher": "npm:0.7.2"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/7fc62240225ce92f6ed5d3ae78f6bb618567f343274fcadae94946692ef7070f887da2fbcba2574d4d664cc0493936f9ae917379a9b83d83653ccaf0c75a1931
checksum: 10/da9a89840e3f9164b4fdcc5b3c05adf9a85a7306c748f42f1cb91a01dd1c69e9f724129556f244ce17b348c545cc4c3b6ab7ae6461f31c852155494f2a324704
languageName: node
linkType: hard
"@formatjs/intl-relativetimeformat@npm:12.0.3":
version: 12.0.3
resolution: "@formatjs/intl-relativetimeformat@npm:12.0.3"
"@formatjs/intl-relativetimeformat@npm:12.0.5":
version: 12.0.5
resolution: "@formatjs/intl-relativetimeformat@npm:12.0.5"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.2"
"@formatjs/ecma402-abstract": "npm:3.0.3"
"@formatjs/intl-localematcher": "npm:0.7.2"
tslib: "npm:^2.8.0"
checksum: 10/00f0e6feedeed9f04624c530e943d28d7e1dd3f2086f2a7717f379ffbe231c6c5c0808706f75bceee4881be01314a4f2aa16db1f3b9f4b206487f355abf938aa
checksum: 10/ade26ba145dc1ae3c76ce179a8f1f7badca57d9b64f9a235298480219d357ad4422962c4fd48f436fb386766448cef403326199ec6ca2ddecbcb5a3031b6cf37
languageName: node
linkType: hard
@@ -2352,10 +2352,10 @@ __metadata:
languageName: node
linkType: hard
"@lokalise/node-api@npm:15.5.0":
version: 15.5.0
resolution: "@lokalise/node-api@npm:15.5.0"
checksum: 10/245e215bc3d4ced54c05e12760ed1f607937f1e285752ad9a87be40b7b34f553a698c960581b0151d13a8bc44e71c86c2bfa930ae889e0b3631d1ff5290e9672
"@lokalise/node-api@npm:15.6.0":
version: 15.6.0
resolution: "@lokalise/node-api@npm:15.6.0"
checksum: 10/29d4497c34fe090d53bd3ca3f4f1924159d79624fe4706eac6e71b656e55f54b30cd73063e942ea4f4a45714586364fc0e964f1ad4fef4dd7d43af96da19dbb2
languageName: node
linkType: hard
@@ -4166,16 +4166,16 @@ __metadata:
languageName: node
linkType: hard
"@sinonjs/fake-timers@npm:^13.0.5":
version: 13.0.5
resolution: "@sinonjs/fake-timers@npm:13.0.5"
"@sinonjs/fake-timers@npm:^15.1.0":
version: 15.1.0
resolution: "@sinonjs/fake-timers@npm:15.1.0"
dependencies:
"@sinonjs/commons": "npm:^3.0.1"
checksum: 10/11ee417968fc4dce1896ab332ac13f353866075a9d2a88ed1f6258f17cc4f7d93e66031b51fcddb8c203aa4d53fd980b0ae18aba06269f4682164878a992ec3f
checksum: 10/f675a7c8d5f48aba52de57e3ddccb3c0b9346d11e15e8994f6293942c7ea54a7182b485f7c3078106fef0feef1e5b289d00aa00ef0dddadf3f91e9c8f338e8e8
languageName: node
linkType: hard
"@sinonjs/samsam@npm:^8.0.1":
"@sinonjs/samsam@npm:^8.0.3":
version: 8.0.3
resolution: "@sinonjs/samsam@npm:8.0.3"
dependencies:
@@ -7102,10 +7102,10 @@ __metadata:
languageName: node
linkType: hard
"diff@npm:^7.0.0":
version: 7.0.0
resolution: "diff@npm:7.0.0"
checksum: 10/e9b8e48d054c9c0c093c65ce8e2637af94b35f2427001607b14e5e0589e534ea3413a7f91ebe6d7c5a1494ace49cb7c7c3972f442ddd96a4767ff091999a082e
"diff@npm:^8.0.2":
version: 8.0.2
resolution: "diff@npm:8.0.2"
checksum: 10/82a2120d3418f97822e17a6044ccd4b99a91e26e145e8698353673d7146bd2d092bbebb79c112aae7badc7b9c526f9098cbe342f96174feb6beabdd2587b3c42
languageName: node
linkType: hard
@@ -9000,15 +9000,15 @@ __metadata:
"@codemirror/view": "npm:6.39.4"
"@date-fns/tz": "npm:1.4.1"
"@egjs/hammerjs": "npm:2.0.17"
"@formatjs/intl-datetimeformat": "npm:7.0.2"
"@formatjs/intl-displaynames": "npm:7.0.2"
"@formatjs/intl-durationformat": "npm:0.8.2"
"@formatjs/intl-getcanonicallocales": "npm:3.0.2"
"@formatjs/intl-listformat": "npm:8.0.2"
"@formatjs/intl-locale": "npm:5.0.2"
"@formatjs/intl-numberformat": "npm:9.0.3"
"@formatjs/intl-pluralrules": "npm:6.0.2"
"@formatjs/intl-relativetimeformat": "npm:12.0.3"
"@formatjs/intl-datetimeformat": "npm:7.0.4"
"@formatjs/intl-displaynames": "npm:7.0.4"
"@formatjs/intl-durationformat": "npm:0.8.4"
"@formatjs/intl-getcanonicallocales": "npm:3.0.3"
"@formatjs/intl-listformat": "npm:8.0.4"
"@formatjs/intl-locale": "npm:5.0.4"
"@formatjs/intl-numberformat": "npm:9.0.5"
"@formatjs/intl-pluralrules": "npm:6.0.4"
"@formatjs/intl-relativetimeformat": "npm:12.0.5"
"@fullcalendar/core": "npm:6.1.19"
"@fullcalendar/daygrid": "npm:6.1.19"
"@fullcalendar/interaction": "npm:6.1.19"
@@ -9022,7 +9022,7 @@ __metadata:
"@lit-labs/virtualizer": "npm:2.1.1"
"@lit/context": "npm:1.1.6"
"@lit/reactive-element": "npm:2.1.1"
"@lokalise/node-api": "npm:15.5.0"
"@lokalise/node-api": "npm:15.6.0"
"@material/chips": "npm:=14.0.0-canary.53b3cad2f.0"
"@material/data-table": "npm:=14.0.0-canary.53b3cad2f.0"
"@material/mwc-base": "npm:0.27.0"
@@ -9123,7 +9123,7 @@ __metadata:
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"
idb-keyval: "npm:6.2.2"
intl-messageformat: "npm:11.0.2"
intl-messageformat: "npm:11.0.4"
js-yaml: "npm:4.1.1"
jsdom: "npm:27.3.0"
jszip: "npm:3.10.1"
@@ -9151,7 +9151,7 @@ __metadata:
rrule: "npm:2.8.1"
rspack-manifest-plugin: "npm:5.2.0"
serve: "npm:14.2.5"
sinon: "npm:21.0.0"
sinon: "npm:21.0.1"
sortablejs: "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch"
stacktrace-js: "npm:2.0.2"
superstruct: "npm:2.0.2"
@@ -9530,15 +9530,15 @@ __metadata:
languageName: node
linkType: hard
"intl-messageformat@npm:11.0.2":
version: 11.0.2
resolution: "intl-messageformat@npm:11.0.2"
"intl-messageformat@npm:11.0.4":
version: 11.0.4
resolution: "intl-messageformat@npm:11.0.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:3.0.2"
"@formatjs/ecma402-abstract": "npm:3.0.3"
"@formatjs/fast-memoize": "npm:3.0.1"
"@formatjs/icu-messageformat-parser": "npm:3.0.2"
"@formatjs/icu-messageformat-parser": "npm:3.0.4"
tslib: "npm:^2.8.0"
checksum: 10/ea21a2cd6afd40c26ca0e675f2271b94f223c1bc89a590f69061e059bf92ed42ecd88f3e8332319f808a16f6ffe5ae82d02b1f314e82e073e510c0586dd0c6e1
checksum: 10/b7ed6afd7dbb6cc755b12b20d0cf9d506d8530947fa0e631e44fac40a45ee59bdcddea4c1c241de133c51c343b2cae941654f3fbd6cb3f9fbd1aedb59a82a9ce
languageName: node
linkType: hard
@@ -12932,16 +12932,16 @@ __metadata:
languageName: node
linkType: hard
"sinon@npm:21.0.0":
version: 21.0.0
resolution: "sinon@npm:21.0.0"
"sinon@npm:21.0.1":
version: 21.0.1
resolution: "sinon@npm:21.0.1"
dependencies:
"@sinonjs/commons": "npm:^3.0.1"
"@sinonjs/fake-timers": "npm:^13.0.5"
"@sinonjs/samsam": "npm:^8.0.1"
diff: "npm:^7.0.0"
"@sinonjs/fake-timers": "npm:^15.1.0"
"@sinonjs/samsam": "npm:^8.0.3"
diff: "npm:^8.0.2"
supports-color: "npm:^7.2.0"
checksum: 10/b460e1cde56fa34ecbebc216522667fa3985444020dc70508177f85d127e6b9a27184c4b3146c575b30ec3091bde508b1373fad306b1e808037a37035e62065d
checksum: 10/b7cc06de384a964636ccdfca56ec0d3d7288608c41ffe1654222ac6de9a40f87df1ddc546abff8684e998d0bf0b152ac1f600fe8d99b31a351725ac37ca5b9a0
languageName: node
linkType: hard