Compare commits

..

5 Commits

Author SHA1 Message Date
Petar Petrov d4b1fe0c7f Roll the energy "Now" view over to the new day at midnight (#52829) 2026-06-24 10:08:41 +02:00
Dmytro Platov 8ecd350e6f Add Zigbee configuration handling and loading state to ZHA dashboard (#52697)
* Add Zigbee configuration handling and loading state to ZHA dashboard

- Introduced `findActiveZhaConfigEntry` function to filter active Zigbee config entries.
- Updated ZHAConfigDashboard to manage loading state and display a spinner while loading.
- Added UI elements for not configured state with appropriate translations.
- Created tests for `findActiveZhaConfigEntry` to ensure correct functionality.

* fix: remove unused config entry logic and update initialization checks

* Restore active config entry filter in _fetchConfigEntry

* Remove redundant config entry ternary in render
2026-06-24 06:48:18 +00:00
renovate[bot] a26de31a2d Update dependency lint-staged to v17.0.8 (#52825)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-24 07:33:20 +03:00
renovate[bot] 77110afc59 Update Node.js to v24.18.0 (#52827)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-24 07:32:55 +03:00
Paul Bottein 7b6e9ba738 Add by entity suggestions to the badge picker (#52733) 2026-06-23 21:07:14 +02:00
31 changed files with 1031 additions and 616 deletions
+1 -1
View File
@@ -1 +1 @@
24.17.0
24.18.0
@@ -0,0 +1,18 @@
diff --git a/lib/cook-raw-quasi.js b/lib/cook-raw-quasi.js
index 3ea8fa7be8e357c1066d7417caeeecd841415208..6bf04ab0bed8897b5ff2898ca835867aec5cee6a 100644
--- a/lib/cook-raw-quasi.js
+++ b/lib/cook-raw-quasi.js
@@ -1,10 +1,11 @@
'use strict';
-function cookRawQuasi({transform}, raw) {
+function cookRawQuasi({transformSync}, raw) {
// This nasty hack is needed until https://github.com/babel/babel/issues/9242 is resolved.
const args = {raw};
- transform('cooked`' + args.raw + '`', {
+ // Babel 8 removed synchronous `transform`; use `transformSync` instead.
+ transformSync('cooked`' + args.raw + '`', {
babelrc: false,
configFile: false,
plugins: [
+28 -1
View File
@@ -84,7 +84,12 @@ module.exports.swcOptions = () => ({
},
});
module.exports.babelOptions = ({ latestBuild, isTestBuild, sw }) => ({
module.exports.babelOptions = ({
latestBuild,
isProdBuild,
isTestBuild,
sw,
}) => ({
babelrc: false,
compact: false,
assumptions: {
@@ -120,6 +125,28 @@ module.exports.babelOptions = ({ latestBuild, isTestBuild, sw }) => ({
ignoreModuleNotFound: true,
},
],
// Minify template literals for production
isProdBuild && [
"template-html-minifier",
{
modules: {
...Object.fromEntries(
["lit", "lit-element", "lit-html"].map((m) => [
m,
[
"html",
{ name: "svg", encapsulation: "svg" },
{ name: "css", encapsulation: "style" },
],
])
),
"@polymer/polymer/lib/utils/html-tag.js": ["html"],
},
strictCSS: true,
htmlMinifier: module.exports.htmlMinifierOptions,
failOnError: false, // we can turn this off in case of false positives
},
],
// Import helpers and regenerator from runtime package.
// `moduleName` is pinned so helpers resolve from `@babel/runtime`: the
// corejs3 polyfill provider above otherwise redirects them to the
@@ -1,52 +0,0 @@
/* global module */
// rspack/webpack loader that minifies the HTML, SVG, and CSS inside lit
// tagged template literals using `minify-literals` (html-minifier-next +
// lightningcss). Replaces the unmaintained babel-plugin-template-html-minifier.
//
// It runs between swc and babel: swc has already stripped TS types and
// decorators (so minify-literals' acorn parser only sees plain ESM), but the
// `html`/`css`/`svg` tagged templates are still intact at ES2021. Running after
// babel instead would miss the legacy build, where babel lowers the templates
// to `_taggedTemplateLiteral()` calls that no longer look like tagged templates.
// minify-literals is ESM-only, so load it via dynamic import from this CJS loader.
let minifyPromise;
const getMinifier = () => {
if (!minifyPromise) {
minifyPromise = import("minify-literals").then((m) => m.minifyHTMLLiterals);
}
return minifyPromise;
};
// HTML options mirror the previous babel-plugin-template-html-minifier config
// (html-minifier-next is option-compatible with html-minifier-terser). CSS in
// css`` templates and inline <style> is handled by minify-literals' lightningcss
// default.
const htmlOptions = {
caseSensitive: true,
collapseWhitespace: true,
conservativeCollapse: true,
decodeEntities: true,
removeComments: true,
removeRedundantAttributes: true,
};
module.exports = function minifyTemplateLiteralsLoader(source, map, meta) {
const callback = this.async();
getMinifier()
.then((minifyHTMLLiterals) =>
minifyHTMLLiterals(source, {
fileName: this.resourcePath,
html: htmlOptions,
})
)
.then((result) => {
if (!result) {
// No tagged templates changed; pass through untouched.
callback(null, source, map, meta);
} else {
callback(null, result.code, result.map ?? map, meta);
}
})
.catch(callback);
};
+18 -29
View File
@@ -67,36 +67,25 @@ const createRspackConfig = ({
{
test: /\.m?js$|\.ts$/,
exclude: /node_modules[\\/]core-js/,
use: (info) =>
[
{
loader: "babel-loader",
options: {
...bundle.babelOptions({
latestBuild,
isTestBuild,
sw: info.issuerLayer === "sw",
}),
cacheDirectory: !isProdBuild,
cacheCompression: false,
},
use: (info) => [
{
loader: "babel-loader",
options: {
...bundle.babelOptions({
latestBuild,
isProdBuild,
isTestBuild,
sw: info.issuerLayer === "sw",
}),
cacheDirectory: !isProdBuild,
cacheCompression: false,
},
// Minify lit html/svg/css tagged template literals for production.
// Must run after swc (TS/decorators stripped, but templates kept at
// ES2021) and before babel — otherwise the legacy build lowers
// html`` to _taggedTemplateLiteral() calls that can no longer be
// matched, leaving legacy templates unminified.
isProdBuild && {
loader: path.join(
__dirname,
"minify-template-literals-loader.cjs"
),
},
{
loader: "builtin:swc-loader",
options: bundle.swcOptions(),
},
].filter(Boolean),
},
{
loader: "builtin:swc-loader",
options: bundle.swcOptions(),
},
],
resolve: {
fullySpecified: false,
},
+3 -3
View File
@@ -158,6 +158,7 @@
"@vitest/coverage-v8": "4.1.9",
"babel-loader": "10.1.1",
"babel-plugin-polyfill-corejs3": "1.0.0",
"babel-plugin-template-html-minifier": "patch:babel-plugin-template-html-minifier@npm%3A4.1.0#~/.yarn/patches/babel-plugin-template-html-minifier-npm-4.1.0-9a3c00055a.patch",
"browserslist-useragent-regexp": "4.1.4",
"del": "8.0.1",
"eslint": "10.5.0",
@@ -182,12 +183,11 @@
"jsdom": "29.1.1",
"jszip": "3.10.1",
"license-checker-rseidelsohn": "5.0.1",
"lint-staged": "17.0.7",
"lint-staged": "17.0.8",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.18.1",
"map-stream": "0.0.7",
"minify-literals": "2.0.2",
"pinst": "3.0.0",
"prettier": "3.8.4",
"rspack-manifest-plugin": "5.2.2",
@@ -217,6 +217,6 @@
},
"packageManager": "yarn@4.17.0",
"volta": {
"node": "24.17.0"
"node": "24.18.0"
}
}
+42 -7
View File
@@ -42,6 +42,12 @@ import {
export const ENERGY_COLLECTION_KEY_PREFIX = "energy_";
// Collection key for the statistics-based energy dashboard views (Overview,
// Electricity, Gas, Water).
export const DEFAULT_ENERGY_COLLECTION_KEY = "energy_dashboard";
// Collection key for the real-time "Now" view (live power + 5-minute stats).
export const DEFAULT_POWER_COLLECTION_KEY = "energy_dashboard_now";
// All collection keys created this session
const energyCollectionKeys = new Set<string | undefined>();
@@ -787,9 +793,30 @@ const findEnergyDataCollection = (
return (hass.connection as any)[key];
};
// When does the collection's day period need to roll over to the next day?
// With `midnightRollover` (the real-time "Now" view) it rolls over right at
// midnight. Otherwise it waits an hour, until the new day's first hourly
// statistic exists — rolling over at midnight would show an empty graph.
export const getNextEnergyPeriodStart = (
midnightRollover: boolean,
now: Date,
locale: HomeAssistant["locale"],
config: HomeAssistant["config"]
): Date => {
const dayEnd = calcDate(now, endOfDay, locale, config);
return midnightRollover ? addMilliseconds(dayEnd, 1) : addHours(dayEnd, 1);
};
export const getEnergyDataCollection = (
hass: HomeAssistant,
options: { prefs?: EnergyPreferences; key?: string } = {}
options: {
prefs?: EnergyPreferences;
key?: string;
// The real-time "Now" view opts in to rolling its day period over at
// midnight rather than an hour later (it shows live data, so it always
// tracks today and never falls back to yesterday in the first hour).
midnightRollover?: boolean;
} = {}
): EnergyCollection => {
const [key, collectionKey] = convertCollectionKeyToConnection(
hass,
@@ -799,6 +826,8 @@ export const getEnergyDataCollection = (
return (hass.connection as any)[key];
}
const midnightRollover = options.midnightRollover ?? false;
energyCollectionKeys.add(collectionKey);
const collection = getCollection<EnergyData>(
@@ -857,12 +886,16 @@ export const getEnergyDataCollection = (
const now = new Date();
const hour = formatTime24h(now, hass.locale, hass.config).split(":")[0];
// Set start to start of today if we have data for today, otherwise yesterday
// Set start to start of today if we have data for today, otherwise yesterday.
// The real-time "Now" view always tracks today; it shows live data even
// before today's first statistic exists, so it never falls back to yesterday.
const preferredPeriod =
(localStorage.getItem(`energy-default-period-${key}`) as DateRange) ||
"today";
const period =
preferredPeriod === "today" && hour === "0" ? "yesterday" : preferredPeriod;
preferredPeriod === "today" && hour === "0" && !midnightRollover
? "yesterday"
: preferredPeriod;
const [start, end] = calcDateRange(hass.locale, hass.config, period);
collection.start = calcDate(start, startOfDay, hass.locale, hass.config);
@@ -886,10 +919,12 @@ export const getEnergyDataCollection = (
collection.refresh();
scheduleUpdatePeriod();
},
addHours(
calcDate(new Date(), endOfDay, hass.locale, hass.config),
1
).getTime() - Date.now() // Switch to next day an hour after the day changed
getNextEnergyPeriodStart(
midnightRollover,
new Date(),
hass.locale,
hass.config
).getTime() - Date.now()
);
};
scheduleUpdatePeriod();
+12
View File
@@ -1,6 +1,7 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import type { LovelaceCardFeatureContext } from "../panels/lovelace/card-features/types";
import type { LovelaceBadgeConfig } from "./lovelace/config/badge";
import type { LovelaceCardConfig } from "./lovelace/config/card";
export interface CustomCardSuggestion<
@@ -10,6 +11,13 @@ export interface CustomCardSuggestion<
config: T;
}
export interface CustomBadgeSuggestion<
T extends LovelaceBadgeConfig = LovelaceBadgeConfig,
> {
label?: string;
config: T;
}
export interface CustomCardEntry {
type: string;
name?: string;
@@ -28,6 +36,10 @@ export interface CustomBadgeEntry {
description?: string;
preview?: boolean;
documentationURL?: string;
getEntitySuggestion?: (
hass: HomeAssistant,
entityId: string
) => CustomBadgeSuggestion | CustomBadgeSuggestion[] | null;
}
export interface CustomCardFeatureEntry {
@@ -13,6 +13,8 @@ import {
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { navigate } from "../../../../../common/navigate";
import { animationStyles } from "../../../../../resources/theme/animations.globals";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
@@ -21,6 +23,7 @@ import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-svg-icon";
import type { ConfigEntry } from "../../../../../data/config_entries";
import { getConfigEntries } from "../../../../../data/config_entries";
@@ -66,20 +69,46 @@ class ZHAConfigDashboard extends LitElement {
protected firstUpdated(changedProperties: PropertyValues<this>) {
super.firstUpdated(changedProperties);
if (this.hass) {
this.hass.loadBackendTranslation("config_panel", "zha", false);
this._fetchConfigEntry();
this._fetchConfiguration();
this._fetchDevicesAndGroups();
if (!this.hass) {
return;
}
if (!isComponentLoaded(this.hass.config, "zha")) {
navigate("/config/integrations", { replace: true });
return;
}
this.hass.loadBackendTranslation("config_panel", "zha", false);
this._load();
}
private async _load(): Promise<void> {
await this._fetchConfigEntry();
if (!this._configEntry) {
return;
}
this._fetchConfiguration();
this._fetchDevicesAndGroups();
}
protected render(): TemplateResult {
const devices = this._configEntry
? Object.values(this.hass.devices).filter((device) =>
device.config_entries.includes(this._configEntry!.entry_id)
)
: [];
if (!this._configEntry) {
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.zha.network.caption")}
back-path="/config"
>
<div class="loading">
<ha-spinner></ha-spinner>
</div>
</hass-subpage>
`;
}
const configEntry = this._configEntry;
const devices = Object.values(this.hass.devices).filter((device) =>
device.config_entries.includes(configEntry.entry_id)
);
const deviceCount = devices.length;
let entityCount = 0;
@@ -463,6 +492,12 @@ class ZHAConfigDashboard extends LitElement {
margin-top: var(--ha-space-6);
}
.loading {
display: flex;
justify-content: center;
padding: var(--ha-space-12);
}
ha-md-list {
background: none;
padding: 0;
-2
View File
@@ -1,2 +0,0 @@
export const DEFAULT_ENERGY_COLLECTION_KEY = "energy_dashboard";
export const DEFAULT_POWER_COLLECTION_KEY = "energy_dashboard_now";
+1 -1
View File
@@ -16,7 +16,7 @@ import "../lovelace/hui-root";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import { DEFAULT_POWER_COLLECTION_KEY } from "./constants";
import { DEFAULT_POWER_COLLECTION_KEY } from "../../data/energy";
@customElement("ha-panel-energy")
class PanelEnergy extends LitElement {
@@ -1,6 +1,8 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
DEFAULT_ENERGY_COLLECTION_KEY,
DEFAULT_POWER_COLLECTION_KEY,
EMPTY_PREFERENCES,
getEnergyDataCollection,
} from "../../../data/energy";
@@ -11,10 +13,6 @@ import type { LovelaceStrategyViewConfig } from "../../../data/lovelace/config/v
import type { LocalizeKeys } from "../../../common/translations/localize";
import type { HomeAssistant } from "../../../types";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
import {
DEFAULT_ENERGY_COLLECTION_KEY,
DEFAULT_POWER_COLLECTION_KEY,
} from "../constants";
import type { EnergyViewPath } from "./energy-cards";
import {
hasDeviceConsumption,
@@ -160,6 +158,9 @@ async function fetchEnergyPrefs(
): Promise<EnergyPreferences> {
const collection = getEnergyDataCollection(hass, {
key: defaultCollection || DEFAULT_ENERGY_COLLECTION_KEY,
// When landing directly on the "Now" view this warms its real-time
// collection, so it must be created with midnight rollover too.
midnightRollover: defaultCollection === DEFAULT_POWER_COLLECTION_KEY,
});
return await new Promise<EnergyPreferences>((resolve) => {
@@ -1,10 +1,12 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { getEnergyDataCollection } from "../../../data/energy";
import {
DEFAULT_ENERGY_COLLECTION_KEY,
getEnergyDataCollection,
} from "../../../data/energy";
import type { HomeAssistant } from "../../../types";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
import type { EnergyViewStrategyConfig } from "./energy-cards";
import { hasWaterSource, isEnergyCardVisible } from "./energy-cards";
@@ -1,10 +1,12 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { getEnergyDataCollection } from "../../../data/energy";
import {
DEFAULT_ENERGY_COLLECTION_KEY,
getEnergyDataCollection,
} from "../../../data/energy";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
import type { EnergyViewStrategyConfig } from "./energy-cards";
import { isEnergyCardVisible } from "./energy-cards";
import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
@@ -1,9 +1,11 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { getEnergyDataCollection } from "../../../data/energy";
import {
DEFAULT_ENERGY_COLLECTION_KEY,
getEnergyDataCollection,
} from "../../../data/energy";
import type { HomeAssistant } from "../../../types";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
import type { EnergyViewStrategyConfig } from "./energy-cards";
import { hasGasSource, isEnergyCardVisible } from "./energy-cards";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
@@ -1,9 +1,11 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { getEnergyDataCollection } from "../../../data/energy";
import {
DEFAULT_ENERGY_COLLECTION_KEY,
getEnergyDataCollection,
} from "../../../data/energy";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
import type { EnergyViewStrategyConfig } from "./energy-cards";
import {
hasGasRateSource,
@@ -32,6 +34,8 @@ export class PowerViewStrategy extends ReactiveElement {
const energyCollection = getEnergyDataCollection(hass, {
key: collectionKey,
// The "Now" view is real-time; roll its day period over at midnight.
midnightRollover: true,
});
if (!energyCollection.prefs) {
await energyCollection.refresh();
@@ -1,11 +1,13 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { getEnergyDataCollection } from "../../../data/energy";
import {
DEFAULT_ENERGY_COLLECTION_KEY,
getEnergyDataCollection,
} from "../../../data/energy";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
import type { EnergyViewStrategyConfig } from "./energy-cards";
import {
hasWaterDevices,
@@ -0,0 +1,20 @@
import type { EntityBadgeConfig } from "../badges/types";
import type { BadgeSuggestion, BadgeSuggestionProvider } from "./types";
export const entityBadgeSuggestions: BadgeSuggestionProvider<EntityBadgeConfig> =
{
getEntitySuggestion(hass, entityId) {
const suggestions: BadgeSuggestion<EntityBadgeConfig>[] = [
{
config: { type: "entity", entity: entityId },
},
{
label: hass.localize(
"ui.panel.lovelace.editor.badge_picker.with_name"
),
config: { type: "entity", entity: entityId, show_name: true },
},
];
return suggestions;
},
};
@@ -0,0 +1,45 @@
import { ensureArray } from "../../../common/array/ensure-array";
import { customBadges } from "../../../data/lovelace_custom_cards";
import type { HomeAssistant } from "../../../types";
import { BADGE_SUGGESTION_PROVIDERS } from "./registry";
import type { BadgeSuggestion } from "./types";
export type { BadgeSuggestion, BadgeSuggestionProvider } from "./types";
export { BADGE_SUGGESTION_PROVIDERS } from "./registry";
export interface BadgeSuggestions {
core: BadgeSuggestion[];
custom: BadgeSuggestion[];
}
export const generateBadgeSuggestions = (
hass: HomeAssistant,
entityId: string | undefined
): BadgeSuggestions => {
if (!entityId || hass.states[entityId] === undefined) {
return { core: [], custom: [] };
}
const core = Object.values(BADGE_SUGGESTION_PROVIDERS).flatMap((provider) => {
try {
return ensureArray(provider.getEntitySuggestion(hass, entityId)) ?? [];
} catch (err) {
// eslint-disable-next-line no-console
console.error("Badge suggestion provider threw:", err);
return [];
}
});
const custom = customBadges.flatMap((badge) => {
if (!badge.getEntitySuggestion) return [];
try {
return ensureArray(badge.getEntitySuggestion(hass, entityId)) ?? [];
} catch (err) {
// eslint-disable-next-line no-console
console.error(
`Custom badge "${badge.type}" getEntitySuggestion threw:`,
err
);
return [];
}
});
return { core, custom };
};
@@ -0,0 +1,9 @@
import { entityBadgeSuggestions } from "./hui-entity-badge-suggestions";
import type { BadgeSuggestionProvider } from "./types";
export const BADGE_SUGGESTION_PROVIDERS: Record<
string,
BadgeSuggestionProvider
> = {
entity: entityBadgeSuggestions,
};
@@ -0,0 +1,18 @@
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
import type { HomeAssistant } from "../../../types";
export interface BadgeSuggestion<
T extends LovelaceBadgeConfig = LovelaceBadgeConfig,
> {
label?: string;
config: T;
}
export interface BadgeSuggestionProvider<
T extends LovelaceBadgeConfig = LovelaceBadgeConfig,
> {
getEntitySuggestion(
hass: HomeAssistant,
entityId: string
): BadgeSuggestion<T> | BadgeSuggestion<T>[] | null;
}
@@ -13,14 +13,12 @@ import type {
GridSourceTypeEnergyPreference,
} from "../../../data/energy";
import { domainToName } from "../../../data/integration";
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import { computeUserInitials } from "../../../data/user";
import type { HomeAssistant } from "../../../types";
import { HELPER_DOMAINS } from "../../config/helpers/const";
import type { EntityBadgeConfig } from "../badges/types";
import type {
AlarmPanelCardConfig,
EntitiesCardConfig,
@@ -315,23 +313,6 @@ export const computeCards = (
];
};
export const computeBadges = (
_states: HassEntities,
entityIds: string[]
): LovelaceBadgeConfig[] => {
const badges: LovelaceBadgeConfig[] = [];
for (const entityId of entityIds) {
const config: EntityBadgeConfig = {
type: "entity",
entity: entityId,
};
badges.push(config);
}
return badges;
};
const computeDefaultViewStates = (
entities: HassEntities,
entityEntries: HomeAssistant["entities"]
@@ -0,0 +1,375 @@
import { mdiClose, mdiViewGridPlus } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeEntityPickerDisplay } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/entity/state-badge";
import "../../../../components/ha-button";
import "../../../../components/ha-combo-box-item";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-ripple";
import "../../../../components/ha-section-title";
import "../../../../components/ha-svg-icon";
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
import { haStyleScrollbar } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import {
generateBadgeSuggestions,
type BadgeSuggestions,
} from "../../badge-suggestions";
import type { BadgeSuggestion } from "../../badge-suggestions/types";
import "../card-editor/hui-suggestion-entity-tree";
import type { HuiSuggestionEntityTree } from "../card-editor/hui-suggestion-entity-tree";
import "./hui-suggestion-badge";
@customElement("hui-badge-suggestion-picker")
export class HuiBadgeSuggestionPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Array, attribute: false })
public prioritizedBadgeTypes?: string[];
@state() private _entityId?: string;
@state() private _narrow = false;
private _narrowMql?: MediaQueryList;
@query("hui-suggestion-entity-tree")
private _entityTree?: HuiSuggestionEntityTree;
public async focus(): Promise<void> {
await this.updateComplete;
await this._entityTree?.focus();
}
public connectedCallback(): void {
super.connectedCallback();
this._narrowMql = matchMedia("(max-width: 600px)");
this._narrow = this._narrowMql.matches;
this._narrowMql.addEventListener("change", this._handleNarrowChange);
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this._narrowMql?.removeEventListener("change", this._handleNarrowChange);
this._narrowMql = undefined;
}
private _handleNarrowChange = (ev: MediaQueryListEvent) => {
this._narrow = ev.matches;
};
// Memoize on scalars so the result stays stable when only hass changes.
// Keeps hui-badge previews from re-rendering on every state tick.
private _computeSuggestions = memoizeOne(
(
entityId: string | undefined,
priorityTypesKey: string
): BadgeSuggestions => {
const { core, custom } = generateBadgeSuggestions(this.hass, entityId);
const priorityTypes = priorityTypesKey
? priorityTypesKey.split("|")
: undefined;
if (!priorityTypes?.length) return { core, custom };
const isPrioritized = (s: BadgeSuggestion) =>
priorityTypes.includes(s.config.type);
return {
core: [
...core.filter(isPrioritized),
...core.filter((s) => !isPrioritized(s)),
],
custom,
};
}
);
protected render() {
const hasEntity = !!this._entityId;
// Tree is rendered unconditionally so its state (filter, expanded
// branches, fuse index) survives the desktop/mobile and tree/suggestions
// switches.
const showTree = !this._narrow || !hasEntity;
const showMain = !this._narrow || hasEntity;
return html`
<div class=${classMap({ sidebar: true, hidden: !showTree })}>
<hui-suggestion-entity-tree
class="tree"
.hass=${this.hass}
.selectedEntityId=${this._entityId}
@entity-picked=${this._handleEntityPicked}
></hui-suggestion-entity-tree>
</div>
<div class=${classMap({ main: true, hidden: !showMain })}>
<div class="content ha-scrollbar">
${this._renderMainContent(hasEntity)}
</div>
</div>
`;
}
private _renderMainContent(
hasEntity: boolean
): TemplateResult | typeof nothing {
if (!hasEntity) return this._renderEmptyState();
const { core, custom } = this._suggestions();
return html`
${this._narrow ? this._renderSelectedEntity() : nothing}
<ha-section-title>
${this.hass.localize(
"ui.panel.lovelace.editor.badge_picker.suggestions_title"
)}
</ha-section-title>
${this._renderSuggestionsGrid(core)}
${custom.length
? html`
<ha-section-title>
${this.hass.localize(
"ui.panel.lovelace.editor.badge_picker.community_title"
)}
</ha-section-title>
${this._renderSuggestionsGrid(custom)}
`
: nothing}
${this._renderBrowseBadge()}
`;
}
private _renderBrowseBadge(): TemplateResult {
return html`
<div class="browse-badge">
<p>
${this.hass.localize(
"ui.panel.lovelace.editor.badge_picker.not_found"
)}
</p>
<ha-button appearance="plain" @click=${this._browseBadges}>
<ha-svg-icon slot="start" .path=${mdiViewGridPlus}></ha-svg-icon>
${this.hass.localize(
"ui.panel.lovelace.editor.badge_picker.browse_badges"
)}
</ha-button>
</div>
`;
}
private _renderSelectedEntity(): TemplateResult {
const stateObj = this.hass.states[this._entityId!];
const { primary, secondary } = stateObj
? computeEntityPickerDisplay(this.hass, stateObj)
: { primary: this._entityId!, secondary: undefined };
return html`
<ha-section-title>
${this.hass.localize(
"ui.panel.lovelace.editor.badge_picker.selected_entity"
)}
</ha-section-title>
<ha-combo-box-item compact class="selected-entity">
${stateObj
? html`<state-badge
slot="start"
.hass=${this.hass}
.stateObj=${stateObj}
></state-badge>`
: nothing}
<span slot="headline">${primary}</span>
${secondary
? html`<span slot="supporting-text">${secondary}</span>`
: nothing}
<ha-icon-button
slot="end"
.label=${this.hass.localize("ui.common.clear")}
.path=${mdiClose}
@click=${this._clearEntity}
></ha-icon-button>
</ha-combo-box-item>
`;
}
private _renderEmptyState(): TemplateResult {
return html`
<div class="content-empty">
<h2>
${this.hass.localize(
"ui.panel.lovelace.editor.badge_picker.content_empty_title"
)}
</h2>
<p>
${this.hass.localize(
"ui.panel.lovelace.editor.badge_picker.content_empty_description"
)}
</p>
<ha-button appearance="plain" @click=${this._browseBadges}>
<ha-svg-icon slot="start" .path=${mdiViewGridPlus}></ha-svg-icon>
${this.hass.localize(
"ui.panel.lovelace.editor.badge_picker.browse_badges"
)}
</ha-button>
</div>
`;
}
private _suggestionKeys = new WeakMap<BadgeSuggestion, string>();
private _suggestionKey = (s: BadgeSuggestion): string => {
let key = this._suggestionKeys.get(s);
if (key === undefined) {
key = JSON.stringify(s.config);
this._suggestionKeys.set(s, key);
}
return key;
};
private _renderSuggestionsGrid(
suggestions: BadgeSuggestion[]
): TemplateResult {
return html`
<div class="suggestions" @pick-badge-suggestion=${this._pickSuggestion}>
${repeat(
suggestions,
this._suggestionKey,
(s: BadgeSuggestion) => html`
<hui-suggestion-badge
.hass=${this.hass}
.suggestion=${s}
></hui-suggestion-badge>
`
)}
</div>
`;
}
private _suggestions(): BadgeSuggestions {
return this._computeSuggestions(
this._entityId,
(this.prioritizedBadgeTypes ?? []).join("|")
);
}
private _browseBadges(): void {
fireEvent(this, "browse-badges", undefined);
}
private _handleEntityPicked(ev: CustomEvent<{ entityId: string }>): void {
this._entityId = ev.detail.entityId;
}
private _clearEntity(): void {
this._entityId = undefined;
}
private _pickSuggestion(
ev: CustomEvent<{ suggestion: BadgeSuggestion }>
): void {
fireEvent(this, "badge-suggestion-picked", {
config: ev.detail.suggestion.config,
});
}
static get styles(): CSSResultGroup {
return [
haStyleScrollbar,
css`
:host {
display: flex;
flex-direction: row;
min-height: 0;
}
.sidebar {
flex: 0 0 320px;
display: flex;
flex-direction: column;
border-inline-end: var(--ha-border-width-sm) solid
var(--divider-color);
min-height: 0;
overflow: hidden;
}
.tree {
flex: 1;
min-height: 0;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.content {
flex: 1;
min-height: 0;
overflow: auto;
}
.hidden {
display: none !important;
}
.suggestions {
display: grid;
gap: var(--ha-space-3);
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
padding: var(--ha-space-3);
}
.content-empty {
box-sizing: border-box;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--ha-space-3);
padding: var(--ha-space-8) var(--ha-space-4);
text-align: center;
}
.content-empty h2 {
margin: 0;
font-size: var(--ha-font-size-xl);
font-weight: var(--ha-font-weight-medium);
}
.content-empty p {
margin: 0;
max-width: 480px;
color: var(--ha-color-text-secondary);
line-height: var(--ha-line-height-expanded);
}
.browse-badge {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--ha-space-2);
padding: var(--ha-space-6) var(--ha-space-4);
}
.browse-badge p {
margin: 0;
color: var(--ha-color-text-secondary);
font-size: var(--ha-font-size-s);
}
/* Mobile master/detail: sidebar OR main is visible, never both. */
@media (max-width: 600px) {
:host {
flex-direction: column;
overflow: hidden;
}
.sidebar {
flex: 1;
border-inline-end: none;
}
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-badge-suggestion-picker": HuiBadgeSuggestionPicker;
}
interface HASSDomEvents {
"browse-badges": undefined;
"badge-suggestion-picked": { config: LovelaceBadgeConfig };
}
}
@@ -3,35 +3,24 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-tab-group";
import "../../../../components/ha-tab-group-tab";
import "../../../../components/ha-dialog";
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { computeBadges } from "../../common/generate-lovelace-config";
import "../card-editor/hui-entity-picker-table";
import { addBadge } from "../config-util";
import { findLovelaceContainer } from "../lovelace-path";
import "./hui-badge-picker";
import "./hui-badge-suggestion-picker";
import type { CreateBadgeDialogParams } from "./show-create-badge-dialog";
import { showEditBadgeDialog } from "./show-edit-badge-dialog";
import { showSuggestBadgeDialog } from "./show-suggest-badge-dialog";
declare global {
interface HASSDomEvents {
"selected-changed": SelectedChangedEvent;
}
}
interface SelectedChangedEvent {
selectedEntities: string[];
}
@customElement("hui-dialog-create-badge")
export class HuiCreateDialogBadge
@@ -46,13 +35,17 @@ export class HuiCreateDialogBadge
@state() private _containerConfig!: LovelaceViewConfig;
@state() private _selectedEntities: string[] = [];
@state() private _currTab: "badge" | "entity" = "entity";
@state() private _currTab: "badge" | "entity" = "badge";
@state() private _narrow = false;
public async showDialog(params: CreateBadgeDialogParams): Promise<void> {
this._params = params;
this._narrow = matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
const containerConfig = findLovelaceContainer(
params.lovelaceConfig,
params.path
@@ -74,8 +67,7 @@ export class HuiCreateDialogBadge
private _dialogClosed(): void {
this._open = false;
this._params = undefined;
this._currTab = "badge";
this._selectedEntities = [];
this._currTab = "entity";
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -98,7 +90,6 @@ export class HuiCreateDialogBadge
width="large"
@keydown=${this._ignoreKeydown}
@closed=${this._dialogClosed}
class=${classMap({ table: this._currTab === "entity" })}
>
<ha-dialog-header show-border slot="header">
<ha-icon-button
@@ -109,6 +100,15 @@ export class HuiCreateDialogBadge
></ha-icon-button>
<span slot="title">${title}</span>
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
<ha-tab-group-tab
slot="nav"
.active=${this._currTab === "entity"}
panel="entity"
?autofocus=${this._narrow}
>${this.hass!.localize(
"ui.panel.lovelace.editor.badge_picker.by_entity"
)}</ha-tab-group-tab
>
<ha-tab-group-tab
slot="nav"
.active=${this._currTab === "badge"}
@@ -118,35 +118,31 @@ export class HuiCreateDialogBadge
"ui.panel.lovelace.editor.badge_picker.by_badge"
)}
</ha-tab-group-tab>
<ha-tab-group-tab
slot="nav"
.active=${this._currTab === "entity"}
panel="entity"
>${this.hass!.localize(
"ui.panel.lovelace.editor.badge_picker.by_entity"
)}</ha-tab-group-tab
>
</ha-tab-group>
</ha-dialog-header>
${cache(
this._currTab === "badge"
? html`
<hui-badge-picker
autofocus
.suggestedBadges=${this._params.suggestedBadges}
.lovelace=${this._params.lovelaceConfig}
.hass=${this.hass}
@config-changed=${this._handleBadgePicked}
></hui-badge-picker>
`
: html`
<hui-entity-picker-table
.hass=${this.hass}
.narrow=${true}
@selected-changed=${this._handleSelectedChanged}
></hui-entity-picker-table>
`
)}
<div class="body">
${cache(
this._currTab === "entity"
? html`
<hui-badge-suggestion-picker
?autofocus=${!this._narrow}
.hass=${this.hass}
.prioritizedBadgeTypes=${this._params.suggestedBadges}
@badge-suggestion-picked=${this._handleSuggestionPicked}
@browse-badges=${this._handleBrowseBadges}
></hui-badge-suggestion-picker>
`
: html`
<hui-badge-picker
?autofocus=${!this._narrow}
.suggestedBadges=${this._params.suggestedBadges}
.lovelace=${this._params.lovelaceConfig}
.hass=${this.hass}
@config-changed=${this._handleBadgePicked}
></hui-badge-picker>
`
)}
</div>
<ha-dialog-footer slot="footer">
<ha-button
@@ -156,13 +152,6 @@ export class HuiCreateDialogBadge
>
${this.hass!.localize("ui.common.cancel")}
</ha-button>
${this._selectedEntities.length
? html`
<ha-button slot="primaryAction" @click=${this._suggestBadges}>
${this.hass!.localize("ui.common.continue")}
</ha-button>
`
: ""}
</ha-dialog-footer>
</ha-dialog>
`;
@@ -181,13 +170,19 @@ export class HuiCreateDialogBadge
--dialog-z-index: 6;
}
ha-dialog.table {
--dialog-content-padding: 0;
@media (min-width: 451px) and (min-height: 501px) {
ha-dialog {
--ha-dialog-min-height: min(900px, 80vh);
--ha-dialog-max-height: var(--ha-dialog-min-height);
}
}
ha-dialog::part(body) {
overflow: hidden;
}
ha-dialog-footer {
border-top: 1px solid var(--divider-color);
}
ha-tab-group-tab {
flex: 1;
@@ -196,34 +191,41 @@ export class HuiCreateDialogBadge
width: 100%;
justify-content: center;
}
.body {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
hui-badge-picker,
hui-badge-suggestion-picker {
flex: 1;
min-height: 0;
}
hui-badge-picker {
--badge-picker-search-shape: 0;
--badge-picker-search-margin: 0;
display: flex;
flex-direction: column;
min-height: 0;
}
hui-badge-picker,
hui-entity-picker-table {
height: calc(100vh - 198px);
}
hui-entity-picker-table {
display: block;
--mdc-shape-small: 0;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
hui-badge-picker,
hui-entity-picker-table {
height: calc(100vh - 158px);
}
}
`,
];
}
private _handleBrowseBadges(): void {
this._currTab = "badge";
}
private async _handleSuggestionPicked(
ev: CustomEvent<{ config: LovelaceBadgeConfig }>
): Promise<void> {
const config = ev.detail.config;
const lovelaceConfig = this._params!.lovelaceConfig;
const containerPath = this._params!.path;
const saveConfig = this._params!.saveConfig;
const newConfig = addBadge(lovelaceConfig, containerPath, config);
await saveConfig(newConfig);
this.closeDialog();
}
private _handleBadgePicked(ev) {
const config = ev.detail.config;
if (this._params!.entities && this._params!.entities.length) {
@@ -249,13 +251,7 @@ export class HuiCreateDialogBadge
if (newTab === this._currTab) {
return;
}
this._currTab = newTab;
this._selectedEntities = [];
}
private _handleSelectedChanged(ev: CustomEvent): void {
this._selectedEntities = ev.detail.selectedEntities;
}
private _cancel(ev?: Event) {
@@ -264,20 +260,6 @@ export class HuiCreateDialogBadge
}
this.closeDialog();
}
private _suggestBadges(): void {
const badgeConfig = computeBadges(this.hass.states, this._selectedEntities);
showSuggestBadgeDialog(this, {
lovelaceConfig: this._params!.lovelaceConfig,
saveConfig: this._params!.saveConfig,
path: this._params!.path as [number],
entities: this._selectedEntities,
badgeConfig,
});
this.closeDialog();
}
}
declare global {
@@ -1,195 +0,0 @@
import deepFreeze from "deep-freeze";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-yaml-editor";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-dialog";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
import "../../badges/hui-badge";
import { addBadges } from "../config-util";
import type { LovelaceContainerPath } from "../lovelace-path";
import { parseLovelaceContainerPath } from "../lovelace-path";
import type { SuggestBadgeDialogParams } from "./show-suggest-badge-dialog";
@customElement("hui-dialog-suggest-badge")
export class HuiDialogSuggestBadge extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: SuggestBadgeDialogParams;
@state() private _open = false;
@state() private _badgeConfig?: LovelaceBadgeConfig[];
@state() private _saving = false;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
public showDialog(params: SuggestBadgeDialogParams): void {
this._params = params;
this._badgeConfig = params.badgeConfig;
this._open = true;
if (!Object.isFrozen(this._badgeConfig)) {
this._badgeConfig = deepFreeze(this._badgeConfig);
}
if (this._yamlEditor) {
this._yamlEditor.setValue(this._badgeConfig);
}
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._open = false;
this._params = undefined;
this._badgeConfig = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _renderPreview() {
if (this._badgeConfig) {
return html`
<div class="element-preview">
${this._badgeConfig.map(
(badgeConfig) => html`
<hui-badge
.hass=${this.hass}
.config=${badgeConfig}
preview
></hui-badge>
`
)}
</div>
`;
}
return nothing;
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-dialog
.open=${this._open}
header-title=${this.hass!.localize(
"ui.panel.lovelace.editor.suggest_badge.header"
)}
@closed=${this._dialogClosed}
>
<div>
${this._renderPreview()}
${this._params.yaml && this._badgeConfig
? html`
<div class="editor">
<ha-yaml-editor
.defaultValue=${this._badgeConfig}
in-dialog
></ha-yaml-editor>
</div>
`
: nothing}
</div>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
autofocus
>
${this._params.yaml
? this.hass!.localize("ui.common.close")
: this.hass!.localize("ui.common.cancel")}
</ha-button>
${!this._params.yaml
? html`
<ha-button
slot="primaryAction"
@click=${this._save}
.loading=${this._saving}
>
${this.hass!.localize(
"ui.panel.lovelace.editor.suggest_badge.add"
)}
</ha-button>
`
: nothing}
</ha-dialog-footer>
</ha-dialog>
`;
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--dialog-z-index: 6;
}
.hidden {
display: none;
}
.element-preview {
position: relative;
display: flex;
align-items: flex-start;
flex-wrap: wrap;
justify-content: center;
gap: var(--ha-space-2);
margin: 0;
}
.editor {
padding-top: 16px;
}
`,
];
}
private _computeNewConfig(
config: LovelaceConfig,
path: LovelaceContainerPath
): LovelaceConfig {
const { viewIndex } = parseLovelaceContainerPath(path);
const newBadges = this._badgeConfig!;
return addBadges(config, [viewIndex], newBadges);
}
private async _save(): Promise<void> {
if (
!this._params?.lovelaceConfig ||
!this._params?.path ||
!this._params?.saveConfig ||
!this._badgeConfig
) {
return;
}
this._saving = true;
const newConfig = this._computeNewConfig(
this._params.lovelaceConfig,
this._params.path
);
await this._params!.saveConfig(newConfig);
this._saving = false;
showSaveSuccessToast(this, this.hass);
this.closeDialog();
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-dialog-suggest-badge": HuiDialogSuggestBadge;
}
}
@@ -0,0 +1,124 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-ripple";
import {
getCustomBadgeEntry,
isCustomType,
stripCustomPrefix,
} from "../../../../data/lovelace_custom_cards";
import type { HomeAssistant } from "../../../../types";
import type { BadgeSuggestion } from "../../badge-suggestions/types";
import "../../badges/hui-badge";
@customElement("hui-suggestion-badge")
export class HuiSuggestionBadge extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public suggestion!: BadgeSuggestion;
protected render(): TemplateResult {
const { suggestion } = this;
const type = suggestion.config.type;
let badgeName: string;
if (isCustomType(type)) {
const customType = stripCustomPrefix(type);
badgeName = getCustomBadgeEntry(customType)?.name ?? customType;
} else {
badgeName =
this.hass.localize(
`ui.panel.lovelace.editor.badge.${type}.name` as any
) || type;
}
const label = suggestion.label
? `${badgeName} - ${suggestion.label}`
: badgeName;
return html`
<div
class="badge"
tabindex="0"
role="button"
aria-label=${label}
@keydown=${this._handleKeyDown}
>
<div class="overlay" @click=${this._handleClick}></div>
<div class="badge-header">${label}</div>
<div class="preview">
<hui-badge
.hass=${this.hass}
.config=${suggestion.config}
preview
></hui-badge>
</div>
<ha-ripple></ha-ripple>
</div>
`;
}
private _handleClick(): void {
fireEvent(this, "pick-badge-suggestion", { suggestion: this.suggestion });
}
private _handleKeyDown(ev: KeyboardEvent): void {
if (ev.key === "Enter" || ev.key === " ") {
ev.preventDefault();
this._handleClick();
}
}
static readonly styles: CSSResultGroup = css`
:host {
display: block;
height: 100%;
}
.badge {
height: 100%;
display: flex;
flex-direction: column;
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
background: var(--primary-background-color);
cursor: pointer;
position: relative;
overflow: hidden;
border: var(--ha-card-border-width, var(--ha-border-width-sm)) solid
var(--ha-card-border-color, var(--divider-color));
}
.badge:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
.overlay {
position: absolute;
inset: 0;
z-index: 1;
border-radius: inherit;
}
.badge-header {
color: var(--ha-card-header-color, var(--primary-text-color));
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
padding: var(--ha-space-3) var(--ha-space-4);
text-align: center;
}
.preview {
pointer-events: none;
margin: var(--ha-space-4);
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hui-suggestion-badge": HuiSuggestionBadge;
}
interface HASSDomEvents {
"pick-badge-suggestion": { suggestion: BadgeSuggestion };
}
}
@@ -1,26 +0,0 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import type { LovelaceContainerPath } from "../lovelace-path";
export interface SuggestBadgeDialogParams {
lovelaceConfig?: LovelaceConfig;
yaml?: boolean;
saveConfig?: (config: LovelaceConfig) => void;
path?: LovelaceContainerPath;
entities?: string[]; // We pass this to create dialog when user chooses "Pick own"
badgeConfig: LovelaceBadgeConfig[]; // We can pass a suggested config
}
const importSuggestBadgeDialog = () => import("./hui-dialog-suggest-badge");
export const showSuggestBadgeDialog = (
element: HTMLElement,
suggestBadgeDialogParams: SuggestBadgeDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "hui-dialog-suggest-badge",
dialogImport: importSuggestBadgeDialog,
dialogParams: suggestBadgeDialogParams,
});
};
@@ -260,4 +260,7 @@ declare global {
interface HTMLElementTagNameMap {
"hui-entity-picker-table": HuiEntityPickerTable;
}
interface HASSDomEvents {
"selected-changed": { selectedEntities: string[] };
}
}
+9 -1
View File
@@ -10489,7 +10489,15 @@
"domain": "Domain",
"entity": "Entity",
"by_entity": "By entity",
"by_badge": "By badge"
"by_badge": "By badge",
"with_name": "With name",
"suggestions_title": "[%key:ui::panel::lovelace::editor::cardpicker::suggestions_title%]",
"community_title": "[%key:ui::panel::lovelace::editor::cardpicker::community_title%]",
"selected_entity": "[%key:ui::panel::lovelace::editor::cardpicker::selected_entity%]",
"content_empty_title": "[%key:ui::panel::lovelace::editor::cardpicker::content_empty_title%]",
"content_empty_description": "Or browse all badge types.",
"browse_badges": "Browse all badges",
"not_found": "Can't find the badge you want?"
},
"header-footer": {
"header": "Header",
+55
View File
@@ -1,5 +1,8 @@
import { startOfDay } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket";
import { assert, describe, it } from "vitest";
import { calcDate } from "../../src/common/datetime/calc_date";
import {
type FrontendLocaleData,
NumberFormat,
@@ -13,6 +16,7 @@ import {
formatConsumptionShort,
calculateSolarConsumedGauge,
formatPowerShort,
getNextEnergyPeriodStart,
} from "../../src/data/energy";
import type { HomeAssistant } from "../../src/types";
@@ -854,3 +858,54 @@ describe("Self-consumed solar gauge tests", () => {
);
});
});
describe("getNextEnergyPeriodStart", () => {
const locale: FrontendLocaleData = {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.language,
date_format: DateFormat.language,
time_zone: TimeZone.server,
first_weekday: FirstWeekday.language,
};
// Pin the time zone (via TimeZone.server) so the test does not depend on the
// machine's local zone.
const config = { time_zone: "America/New_York" } as HassConfig;
const isMidnight = (date: Date) =>
calcDate(date, startOfDay, locale, config).getTime() === date.getTime();
it("rolls the real-time view over at midnight, statistics an hour later", () => {
const now = new Date("2026-06-19T15:30:00-04:00");
const realTime = getNextEnergyPeriodStart(true, now, locale, config);
const statistics = getNextEnergyPeriodStart(false, now, locale, config);
// Real-time rolls over exactly at the next midnight.
assert.isTrue(isMidnight(realTime));
assert.equal(
realTime.getTime(),
new Date("2026-06-20T00:00:00-04:00").getTime()
);
// Statistics roll over an hour after midnight, on the same day boundary.
assert.equal(statistics.getTime() - realTime.getTime(), 60 * 60 * 1000 - 1);
assert.equal(
calcDate(statistics, startOfDay, locale, config).getTime(),
realTime.getTime()
);
});
it("advances the real-time view to the next midnight when called after midnight", () => {
const now = new Date("2026-06-20T00:30:00-04:00");
const realTime = getNextEnergyPeriodStart(true, now, locale, config);
assert.isTrue(isMidnight(realTime));
// Next midnight is June 21, not the already-passed June 20 midnight.
assert.equal(
realTime.getTime(),
new Date("2026-06-21T00:00:00-04:00").getTime()
);
});
});
+99 -158
View File
@@ -5108,15 +5108,6 @@ __metadata:
languageName: node
linkType: hard
"@sveltejs/acorn-typescript@npm:^1.0.10":
version: 1.0.10
resolution: "@sveltejs/acorn-typescript@npm:1.0.10"
peerDependencies:
acorn: ^8.9.0
checksum: 10/5770f9bdcfdac2c5454318fffb8ba0a7ddbdd002221016d2e32855588bb5727d09cafc48923ac55404f5e4680f7a844599b91987931445fec608428137b38462
languageName: node
linkType: hard
"@swc/helpers@npm:0.5.23":
version: 0.5.23
resolution: "@swc/helpers@npm:0.5.23"
@@ -6300,7 +6291,7 @@ __metadata:
languageName: node
linkType: hard
"acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.15.0, acorn@npm:^8.16.0":
"acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.5.0":
version: 8.17.0
resolution: "acorn@npm:8.17.0"
bin:
@@ -6778,6 +6769,28 @@ __metadata:
languageName: node
linkType: hard
"babel-plugin-template-html-minifier@npm:4.1.0":
version: 4.1.0
resolution: "babel-plugin-template-html-minifier@npm:4.1.0"
dependencies:
clean-css: "npm:^4.2.1"
html-minifier-terser: "npm:^5.0.0"
is-builtin-module: "npm:^3.0.0"
checksum: 10/d7582da510cbb947cdc06accfcc03b8da89e7634e0890902ccc8ba55467eb43a312bd5833362e40f3c6aa1b22bafba84d51f6d6d84232c17f10c59f017ac52c6
languageName: node
linkType: hard
"babel-plugin-template-html-minifier@patch:babel-plugin-template-html-minifier@npm%3A4.1.0#~/.yarn/patches/babel-plugin-template-html-minifier-npm-4.1.0-9a3c00055a.patch":
version: 4.1.0
resolution: "babel-plugin-template-html-minifier@patch:babel-plugin-template-html-minifier@npm%3A4.1.0#~/.yarn/patches/babel-plugin-template-html-minifier-npm-4.1.0-9a3c00055a.patch::version=4.1.0&hash=4a6de3"
dependencies:
clean-css: "npm:^4.2.1"
html-minifier-terser: "npm:^5.0.0"
is-builtin-module: "npm:^3.0.0"
checksum: 10/cd119f593f1228f13c9beb934ec2a8479d2403b12b7215b1df839a4e831fc627711ca124d1c9300b9d971f18e0b2f48ffe59310033f469e4a176a7c290dcc10b
languageName: node
linkType: hard
"bach@npm:^2.0.1":
version: 2.0.1
resolution: "bach@npm:2.0.1"
@@ -6912,13 +6925,6 @@ __metadata:
languageName: node
linkType: hard
"boolbase@npm:^1.0.0":
version: 1.0.0
resolution: "boolbase@npm:1.0.0"
checksum: 10/3e25c80ef626c3a3487c73dbfc70ac322ec830666c9ad915d11b701142fab25ec1e63eff2c450c74347acfd2de854ccde865cd79ef4db1683f7c7b046ea43bb0
languageName: node
linkType: hard
"bottleneck@npm:^2.15.3":
version: 2.19.5
resolution: "bottleneck@npm:2.19.5"
@@ -7057,6 +7063,13 @@ __metadata:
languageName: node
linkType: hard
"builtin-modules@npm:^3.3.0":
version: 3.3.0
resolution: "builtin-modules@npm:3.3.0"
checksum: 10/62e063ab40c0c1efccbfa9ffa31873e4f9d57408cb396a2649981a0ecbce56aabc93c28feaccbc5658c95aab2703ad1d11980e62ec2e5e72637404e1eb60f39e
languageName: node
linkType: hard
"bytes@npm:3.0.0":
version: 3.0.0
resolution: "bytes@npm:3.0.0"
@@ -7137,7 +7150,7 @@ __metadata:
languageName: node
linkType: hard
"camel-case@npm:^4.1.2":
"camel-case@npm:^4.1.1, camel-case@npm:^4.1.2":
version: 4.1.2
resolution: "camel-case@npm:4.1.2"
dependencies:
@@ -7448,13 +7461,6 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^11.1.0":
version: 11.1.0
resolution: "commander@npm:11.1.0"
checksum: 10/66bd2d8a0547f6cb1d34022efb25f348e433b0e04ad76a65279b1b09da108f59a4d3001ca539c60a7a46ea38bcf399fc17d91adad76a8cf43845d8dcbaf5cda1
languageName: node
linkType: hard
"commander@npm:^14.0.2":
version: 14.0.3
resolution: "commander@npm:14.0.3"
@@ -7462,13 +7468,6 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^15.0.0":
version: 15.0.0
resolution: "commander@npm:15.0.0"
checksum: 10/a5dab1f5c3f1bf2ea19e8089f650f7fb65c3ed88e13ddde62e490d34b20057bcb2cd3de5b6ddc8aae93286083f8256ca0b812df842f574cbebe0e40f5b2f98f7
languageName: node
linkType: hard
"commander@npm:^2.20.0, commander@npm:^2.20.3":
version: 2.20.3
resolution: "commander@npm:2.20.3"
@@ -7476,6 +7475,13 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^4.1.1":
version: 4.1.1
resolution: "commander@npm:4.1.1"
checksum: 10/3b2dc4125f387dab73b3294dbcb0ab2a862f9c0ad748ee2b27e3544d25325b7a8cdfbcc228d103a98a716960b14478114a5206b5415bd48cdafa38797891562c
languageName: node
linkType: hard
"comment-parser@npm:^1.4.1":
version: 1.4.7
resolution: "comment-parser@npm:1.4.7"
@@ -7664,20 +7670,7 @@ __metadata:
languageName: node
linkType: hard
"css-select@npm:^5.1.0":
version: 5.2.2
resolution: "css-select@npm:5.2.2"
dependencies:
boolbase: "npm:^1.0.0"
css-what: "npm:^6.1.0"
domhandler: "npm:^5.0.2"
domutils: "npm:^3.0.1"
nth-check: "npm:^2.0.1"
checksum: 10/ebb6a88446433312d1a16301afd1c5f75090805b730dbbdccb0338b0d6ca7922410375f16dde06673ef7da086e2cf3b9ad91afe9a8e0d2ee3625795cb5e0170d
languageName: node
linkType: hard
"css-tree@npm:^3.0.0, css-tree@npm:^3.0.1, css-tree@npm:^3.1.0, css-tree@npm:^3.2.1":
"css-tree@npm:^3.0.0, css-tree@npm:^3.1.0, css-tree@npm:^3.2.1":
version: 3.2.1
resolution: "css-tree@npm:3.2.1"
dependencies:
@@ -7687,23 +7680,6 @@ __metadata:
languageName: node
linkType: hard
"css-tree@npm:~2.2.0":
version: 2.2.1
resolution: "css-tree@npm:2.2.1"
dependencies:
mdn-data: "npm:2.0.28"
source-map-js: "npm:^1.0.1"
checksum: 10/1959c4b0e268bf8db1b3a1776a5ba9ae3a464ccd1226bfa62799cb0a3d0039006e21fb95cec4dec9d687a9a9b90f692dff2d230b631527ece700f4bfb419aaf3
languageName: node
linkType: hard
"css-what@npm:^6.1.0":
version: 6.2.2
resolution: "css-what@npm:6.2.2"
checksum: 10/3c5a53be94728089bd1716f915f7f96adde5dd8bf374610eb03982266f3d860bf1ebaf108cda30509d02ef748fe33eaa59aa75911e2c49ee05a85ef1f9fb5223
languageName: node
linkType: hard
"cssesc@npm:^3.0.0":
version: 3.0.0
resolution: "cssesc@npm:3.0.0"
@@ -7720,15 +7696,6 @@ __metadata:
languageName: node
linkType: hard
"csso@npm:^5.0.5":
version: 5.0.5
resolution: "csso@npm:5.0.5"
dependencies:
css-tree: "npm:~2.2.0"
checksum: 10/4036fb2b9f8ed6b948349136b39e0b19ffb5edee934893a37b55e9a116186c4ae2a9d3ba66fbdbc07fa44a853fb478cd2d8733e4743473dcd364e7f21444ff34
languageName: node
linkType: hard
"culori@npm:4.0.2":
version: 4.0.2
resolution: "culori@npm:4.0.2"
@@ -8039,7 +8006,7 @@ __metadata:
languageName: node
linkType: hard
"domutils@npm:^3.0.1, domutils@npm:^3.2.1":
"domutils@npm:^3.2.1":
version: 3.2.2
resolution: "domutils@npm:3.2.2"
dependencies:
@@ -9637,6 +9604,15 @@ __metadata:
languageName: node
linkType: hard
"he@npm:^1.2.0":
version: 1.2.0
resolution: "he@npm:1.2.0"
bin:
he: bin/he
checksum: 10/d09b2243da4e23f53336e8de3093e5c43d2c39f8d0d18817abfa32ce3e9355391b2edb4bb5edc376aea5d4b0b59d6a0482aab4c52bc02ef95751e4b818e847f1
languageName: node
linkType: hard
"hls.js@npm:1.6.16":
version: 1.6.16
resolution: "hls.js@npm:1.6.16"
@@ -9728,6 +9704,7 @@ __metadata:
"@webcomponents/webcomponentsjs": "npm:2.8.0"
babel-loader: "npm:10.1.1"
babel-plugin-polyfill-corejs3: "npm:1.0.0"
babel-plugin-template-html-minifier: "patch:babel-plugin-template-html-minifier@npm%3A4.1.0#~/.yarn/patches/babel-plugin-template-html-minifier-npm-4.1.0-9a3c00055a.patch"
barcode-detector: "npm:3.2.0"
browserslist-useragent-regexp: "npm:4.1.4"
cally: "npm:0.9.2"
@@ -9776,7 +9753,7 @@ __metadata:
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
leaflet.markercluster: "npm:1.5.3"
license-checker-rseidelsohn: "npm:5.0.1"
lint-staged: "npm:17.0.7"
lint-staged: "npm:17.0.8"
lit: "npm:3.3.3"
lit-analyzer: "npm:2.0.3"
lit-html: "npm:3.3.3"
@@ -9786,7 +9763,6 @@ __metadata:
map-stream: "npm:0.0.7"
marked: "npm:18.0.5"
memoize-one: "npm:6.0.0"
minify-literals: "npm:2.0.2"
node-vibrant: "npm:4.0.4"
object-hash: "npm:3.0.0"
pinst: "npm:3.0.0"
@@ -9865,27 +9841,6 @@ __metadata:
languageName: node
linkType: hard
"html-minifier-next@npm:^6.2.8":
version: 6.2.11
resolution: "html-minifier-next@npm:6.2.11"
dependencies:
commander: "npm:^15.0.0"
entities: "npm:^8.0.0"
lightningcss: "npm:^1.32.0"
svgo: "npm:^4.0.1"
terser: "npm:^5.47.1"
peerDependencies:
"@swc/core": ^1.15.7
peerDependenciesMeta:
"@swc/core":
optional: true
bin:
hmn: cli.js
html-minifier-next: cli.js
checksum: 10/d2b3923f77fff918ab09a9cfdfc3819b89dd928e1e64c60dcb39a2b08ddf6bbd3c696d90e8feb830fed11cd5d5757b271de65cb9d1713ddc171ee9668708eda6
languageName: node
linkType: hard
"html-minifier-terser@npm:7.2.0":
version: 7.2.0
resolution: "html-minifier-terser@npm:7.2.0"
@@ -9903,6 +9858,23 @@ __metadata:
languageName: node
linkType: hard
"html-minifier-terser@npm:^5.0.0":
version: 5.1.1
resolution: "html-minifier-terser@npm:5.1.1"
dependencies:
camel-case: "npm:^4.1.1"
clean-css: "npm:^4.2.3"
commander: "npm:^4.1.1"
he: "npm:^1.2.0"
param-case: "npm:^3.0.3"
relateurl: "npm:^0.2.7"
terser: "npm:^4.6.3"
bin:
html-minifier-terser: cli.js
checksum: 10/97d45614e8f07ba66ea66015cfa80759e3e270b475430f8a5d67586876deaad535db97be3247dee3dd2ed51aeafcd1d6bfaf02276fec12e56cf5e4141e52ae28
languageName: node
linkType: hard
"html-standard@npm:^0.0.13":
version: 0.0.13
resolution: "html-standard@npm:0.0.13"
@@ -10202,6 +10174,15 @@ __metadata:
languageName: node
linkType: hard
"is-builtin-module@npm:^3.0.0":
version: 3.2.1
resolution: "is-builtin-module@npm:3.2.1"
dependencies:
builtin-modules: "npm:^3.3.0"
checksum: 10/e8f0ffc19a98240bda9c7ada84d846486365af88d14616e737d280d378695c8c448a621dcafc8332dbf0fcd0a17b0763b845400709963fa9151ddffece90ae88
languageName: node
linkType: hard
"is-callable@npm:^1.2.7":
version: 1.2.7
resolution: "is-callable@npm:1.2.7"
@@ -11167,9 +11148,9 @@ __metadata:
languageName: node
linkType: hard
"lint-staged@npm:17.0.7":
version: 17.0.7
resolution: "lint-staged@npm:17.0.7"
"lint-staged@npm:17.0.8":
version: 17.0.8
resolution: "lint-staged@npm:17.0.8"
dependencies:
listr2: "npm:^10.2.1"
picomatch: "npm:^4.0.4"
@@ -11181,7 +11162,7 @@ __metadata:
optional: true
bin:
lint-staged: bin/lint-staged.js
checksum: 10/4ed3cd01caa78ff5cc5da7ec69f77f091c43a0d5cbb1e084f7ffd3872a9e599675fb8b5f11fd5911faee0d330952889dd0e14378a26620d8f529eae401ce49b4
checksum: 10/2b574a3107c030e27ff1c34166ef49f2189c256bb423b0deabef0becdf13ed4cfdcc6fb6815a1285ce0daa92fc6c545d8d0245c9d47a8eb3fbccbc4cf3754587
languageName: node
linkType: hard
@@ -11477,13 +11458,6 @@ __metadata:
languageName: node
linkType: hard
"mdn-data@npm:2.0.28":
version: 2.0.28
resolution: "mdn-data@npm:2.0.28"
checksum: 10/aec475e0c078af00498ce2f9434d96a1fdebba9814d14b8f72cd6d5475293f4b3972d0538af2d5c5053d35e1b964af08b7d162b98e9846e9343990b75e4baef1
languageName: node
linkType: hard
"mdn-data@npm:2.27.1":
version: 2.27.1
resolution: "mdn-data@npm:2.27.1"
@@ -11582,19 +11556,6 @@ __metadata:
languageName: node
linkType: hard
"minify-literals@npm:2.0.2":
version: 2.0.2
resolution: "minify-literals@npm:2.0.2"
dependencies:
"@sveltejs/acorn-typescript": "npm:^1.0.10"
acorn: "npm:^8.16.0"
html-minifier-next: "npm:^6.2.8"
lightningcss: "npm:^1.32.0"
magic-string: "npm:^0.30.21"
checksum: 10/51de4b6affcebe082f00709241b8d1587aff06164cf9fd5797ffc45b322c14d0f77e9085171bd78cc35c5a9b0a22c5211f36d177a50fe5d0f8c57e6c10735838
languageName: node
linkType: hard
"minimatch@npm:3.1.5":
version: 3.1.5
resolution: "minimatch@npm:3.1.5"
@@ -12020,15 +11981,6 @@ __metadata:
languageName: node
linkType: hard
"nth-check@npm:^2.0.1":
version: 2.1.1
resolution: "nth-check@npm:2.1.1"
dependencies:
boolbase: "npm:^1.0.0"
checksum: 10/5afc3dafcd1573b08877ca8e6148c52abd565f1d06b1eb08caf982e3fa289a82f2cae697ffb55b5021e146d60443f1590a5d6b944844e944714a5b549675bcd3
languageName: node
linkType: hard
"object-assign@npm:^4":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
@@ -12285,7 +12237,7 @@ __metadata:
languageName: node
linkType: hard
"param-case@npm:^3.0.4":
"param-case@npm:^3.0.3, param-case@npm:^3.0.4":
version: 3.0.4
resolution: "param-case@npm:3.0.4"
dependencies:
@@ -13395,13 +13347,6 @@ __metadata:
languageName: node
linkType: hard
"sax@npm:^1.5.0":
version: 1.6.0
resolution: "sax@npm:1.6.0"
checksum: 10/0909cedcd9f011ceeac80c0240a92d64ef712cf6c04e0f6ee236a8d812f86a59f61bee6bb5da28d75306db050b99e0593051ea77351795822253b984af6cf044
languageName: node
linkType: hard
"saxes@npm:^6.0.0":
version: 6.0.0
resolution: "saxes@npm:6.0.0"
@@ -13782,14 +13727,14 @@ __metadata:
languageName: node
linkType: hard
"source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.1":
"source-map-js@npm:^1.2.1":
version: 1.2.1
resolution: "source-map-js@npm:1.2.1"
checksum: 10/ff9d8c8bf096d534a5b7707e0382ef827b4dd360a577d3f34d2b9f48e12c9d230b5747974ee7c607f0df65113732711bb701fe9ece3c7edbd43cb2294d707df3
languageName: node
linkType: hard
"source-map-support@npm:~0.5.20":
"source-map-support@npm:~0.5.12, source-map-support@npm:~0.5.20":
version: 0.5.21
resolution: "source-map-support@npm:0.5.21"
dependencies:
@@ -13806,7 +13751,7 @@ __metadata:
languageName: node
linkType: hard
"source-map@npm:^0.6.0, source-map@npm:~0.6.0":
"source-map@npm:^0.6.0, source-map@npm:~0.6.0, source-map@npm:~0.6.1":
version: 0.6.1
resolution: "source-map@npm:0.6.1"
checksum: 10/59ef7462f1c29d502b3057e822cdbdae0b0e565302c4dd1a95e11e793d8d9d62006cdc10e0fd99163ca33ff2071360cf50ee13f90440806e7ed57d81cba2f7ff
@@ -14277,23 +14222,6 @@ __metadata:
languageName: node
linkType: hard
"svgo@npm:^4.0.1":
version: 4.0.1
resolution: "svgo@npm:4.0.1"
dependencies:
commander: "npm:^11.1.0"
css-select: "npm:^5.1.0"
css-tree: "npm:^3.0.1"
css-what: "npm:^6.1.0"
csso: "npm:^5.0.5"
picocolors: "npm:^1.1.1"
sax: "npm:^1.5.0"
bin:
svgo: ./bin/svgo.js
checksum: 10/8791aa12f3d1a5b3da12a67c2f880917512eaf32dad40563ae474deefff0630a4ce2259e06730f02150756ac77cc8b06598d30fb3ed3f02f085e6cbfbd344fb6
languageName: node
linkType: hard
"symbol-tree@npm:^3.2.4":
version: 3.2.4
resolution: "symbol-tree@npm:3.2.4"
@@ -14402,7 +14330,20 @@ __metadata:
languageName: node
linkType: hard
"terser@npm:^5.15.1, terser@npm:^5.17.4, terser@npm:^5.31.1, terser@npm:^5.47.1":
"terser@npm:^4.6.3":
version: 4.8.1
resolution: "terser@npm:4.8.1"
dependencies:
commander: "npm:^2.20.0"
source-map: "npm:~0.6.1"
source-map-support: "npm:~0.5.12"
bin:
terser: bin/terser
checksum: 10/f58024a8bbf08d6421aea69b14f95da2a6e85a6d9a8b93895379084bd39ea70755d82f8676e9a56fde35ebaefbcb7b5d7920af537ffa1b87f638d39608941ea9
languageName: node
linkType: hard
"terser@npm:^5.15.1, terser@npm:^5.17.4, terser@npm:^5.31.1":
version: 5.48.0
resolution: "terser@npm:5.48.0"
dependencies: