Compare commits

...

31 Commits

Author SHA1 Message Date
Paul Bottein c93661eaf7 Clean up retro theme and fix color scale issues
- Add missing ha-font-family-longform override
- Fix light neutral scale contrast (text-disabled, on-disabled tokens)
- Fix dark neutral scale to be monotonically ascending
- Adjust dark primary-90/95 for visible selection fills
- Move lovelace teal to lovelace-background, use gray for pages
- Remove redundant derived variables (mdc-*, switch-*, table-*, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 13:18:25 +02:00
Paul Bottein 7dac2e243f Improve theme 2026-03-31 13:18:25 +02:00
Paul Bottein f9e1023a2e Rename windows-98 feature to retro to avoid trademark issues
Renames all component files, class names, element tags, translation
keys, theme names, and storage keys from windows-98/Windows 98 to
retro/Retro.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 13:18:25 +02:00
Paul Bottein 82a0d9a413 Add translations 2026-03-31 13:18:25 +02:00
Paul Bottein 1325acdba3 Add BSOD 2026-03-31 13:18:25 +02:00
Paul Bottein 07878563be Add labs feature toggle, translations, and random tips
- Subscribe to frontend.windows_98 lab feature to enable/disable
- Move tips to lazy-loaded translation fragment (ui.panel.windows_98)
- Randomize tip selection
- Sort windows_98 with other fun features in labs page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 13:18:25 +02:00
Paul Bottein b0d5aa5e27 Add Clippy-style home automation tip
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 13:18:25 +02:00
Paul Bottein 444f98df66 Remove outdated right-click tip
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 13:18:25 +02:00
Paul Bottein f5e62a2c7d Add Windows 98 Easter egg with Casita assistant
Adds a Clippy-inspired interactive house character (Casita) that applies
a full Windows 98 theme. Features draggable positioning, speech bubble
with fun tips, idle/sleep animations, and a dismiss button. Theme is
enforced via MutationObserver to survive theme mixin overwrites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 13:18:25 +02:00
Norbert Rittel 32b9676f97 Change picker descriptions of triggers to match new style (#51294) 2026-03-31 10:06:47 +02:00
Petar Petrov 7876642f35 Fix x-axis labels for statistics graph month/year periods (#51295) 2026-03-31 10:01:52 +02:00
Paul Bottein 0e3bcfad5e Hide section when all cards are hidden (#51281) 2026-03-31 08:38:09 +02:00
Florent L. cd1c273d5a Add people at home summary tile to home overview dashboard (#30408)
* Add persons summary tile to home overview dashboard

Show how many people are currently home in the Summary section
of the default home dashboard. Only persons with at least one
tracking device are included. The tile only appears when the map
panel is loaded and at least one tracked person entity exists.
Tapping navigates to the map panel. Displays a count of persons
home or "Nobody" when all are away.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove persons tile from home overview strategy

* Translation tweak

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-03-31 07:56:44 +03:00
Louis Sautier d92ac4b4b7 Add solo-select gesture to chart legend (#30395)
* Add solo-select gesture to chart legend

Ctrl+click (Cmd+click on Mac) or long-press (touch, 500ms) a legend
item to solo-select it:
- Solo-click any item → hide everything else, show only that item
- Solo-click the only visible item → restore all

There is no special "solo mode" — the gesture simply sets which items
are hidden. Normal click/tap continues to toggle individual series,
including after a solo action (e.g. solo a, then click b to add it).

Closes https://github.com/orgs/home-assistant/discussions/1492

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Deduplicate legend parsing in _renderLegend and _getAllLegendIds

Both methods parsed options.legend and filtered datasets identically.
Extract the shared logic into a new _getLegendItems method.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update src/components/chart/ha-chart-base.ts

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-03-30 15:08:16 +00:00
Petar Petrov bfecb1d4a9 Disable physics by default for large networks (#51277) 2026-03-30 14:22:11 +00:00
Wendelin 69a8db00fa Fix ha-dropdown z-index for legacy browsers (#51276) 2026-03-30 13:41:44 +00:00
Maarten Lakerveld bbda7affdc Add ability to duplicate a section (#30265)
* Add ability to duplicate a section

* Move section edit mode buttons to overflow menu

* Fix typing for concat and push parameters

* Fix incorrect clipboard typing for badges
2026-03-30 14:59:27 +02:00
Aidan Timson 10c90d222d Limit ha-toast width to window, refactor CSS (#51272)
* Limit `ha-toast` width to window and use safe width

* Query assigned slots to stop actions display

* Constrain max-width

* Increase start/end padding
2026-03-30 15:31:03 +03:00
Bram Kragten 072f70b49f Numeric threshold selector: remove duplicate uom from input (#51275) 2026-03-30 14:12:04 +02:00
Wendelin 7f2a5ecc27 Remove mobile-specific styles for date-range-picker (#51273)
Remove mobile-specific styles for date-picker component
2026-03-30 13:03:21 +02:00
Paul Bottein a42f6f864a Reduce heading button badge font size and fix alignement (#51274)
Title: Reduce heading button badge font size and fix alignement
2026-03-30 13:02:06 +02:00
Paulus Schoutsen a07772c514 Reload the app info after an update completes (#51261) 2026-03-30 10:42:16 +01:00
Tom Carpenter a6ab6e218f Fix new date-range-picker rendering on small screens (#51257) 2026-03-30 11:10:06 +02:00
ildar170975 ed96657085 Add getContrastedColorHex() to be used for contrasted text & background (or vice versa) (#29032)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-03-30 10:57:28 +02:00
dependabot[bot] 50ca39722e Bump github/codeql-action from 4.34.1 to 4.35.1 (#51268)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 08:43:59 +01:00
Wendelin 7026e5b375 Fix date-range-picker preset selection (#51269) 2026-03-30 09:40:40 +02:00
Wendelin 37e8e1b728 Fix time input background (#51270)
Fix input color tokens
2026-03-30 09:39:57 +02:00
Paulus Schoutsen 48369854af link to supervisor logs on app install error (#51259) 2026-03-30 09:00:21 +02:00
Tom Carpenter 7715e01126 Add date range picker time validation (#51267)
* Fix base time inputs reportValidity() function

The queryAll selector returns a NodeList not not an array. Need to spread it to an array before we can use every().

* Validate the date range picker time inputs

Enable auto validation to get the nice red underline on invalid values, and then check validity before accepting the input.

* Fix automatic 24hr value conversion in AM/PM format

When using AM/PM, entering a 24 hour value will automatically convert the first time. For example 15 will become 3. However if you then enter 15 again it will stay as 15 and not update.
To fix this, make sure we trigger an update of the input field once the current update cycle is complete.

* Validate time inputs on save not value update

In the value changed callback, the update 24->12hr input correction will not have been updated and therefore they will report invalid.
2026-03-30 09:13:51 +03:00
renovate[bot] e4ee108e14 Update vitest monorepo to v4.1.2 (#51265)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-29 17:25:23 +00:00
renovate[bot] 407609c118 Update dependency @swc/helpers to v0.5.20 (#51264)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-29 17:25:18 +00:00
53 changed files with 1777 additions and 357 deletions
+3 -3
View File
@@ -41,14 +41,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -62,4 +62,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
+3 -3
View File
@@ -82,7 +82,7 @@
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.19",
"@swc/helpers": "0.5.20",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0",
@@ -169,7 +169,7 @@
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.1.1",
"@vitest/coverage-v8": "4.1.2",
"babel-loader": "10.1.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
@@ -210,7 +210,7 @@
"typescript": "5.9.3",
"typescript-eslint": "8.57.2",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.1",
"vitest": "4.1.2",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.4.0#~/.yarn/patches/workbox-build-npm-7.4.0-c84561662c.patch"
+28
View File
@@ -32,6 +32,12 @@ const YAML_ONLY_THEMES_COLORS = new Set([
"disabled",
]);
/**
* Compose a CSS variable out of a theme color
* @param color - Theme color (examples: `red`, `primary-text`)
* @returns CSS variable in `--xxx-color` format;
* initial color if not found in theme colors
*/
export function computeCssVariableName(color: string): string {
if (THEME_COLORS.has(color) || YAML_ONLY_THEMES_COLORS.has(color)) {
return `--${color}-color`;
@@ -39,6 +45,12 @@ export function computeCssVariableName(color: string): string {
return color;
}
/**
* Compose a CSS variable out of a theme color & then resolve it
* @param color - Theme color (examples: `red`, `primary-text`)
* @returns Resolved CSS variable in `var(--xxx-color)` format;
* initial color if not found in theme colors
*/
export function computeCssColor(color: string): string {
const cssVarName = computeCssVariableName(color);
if (cssVarName !== color) {
@@ -47,6 +59,22 @@ export function computeCssColor(color: string): string {
return color;
}
/**
* Get a color from document's styles
* @param color - Named theme color (examples: `red`, `primary-text`)
* @returns Resolved color; initial color if not found in document's styles
*/
export function resolveThemeColor(color: string): string {
const cssColor = computeCssVariableName(color);
if (cssColor.startsWith("--")) {
const resolved = getComputedStyle(document.body)
.getPropertyValue(cssColor)
.trim();
return resolved || color;
}
return cssColor;
}
/**
* Validates if a string is a valid color.
* Accepts: hex colors (#xxx, #xxxxxx), theme colors, and valid CSS color names.
+30 -12
View File
@@ -1,5 +1,6 @@
import colors from "color-name";
import { expandHex } from "./hex";
import { resolveThemeColor } from "./compute-color";
const rgb_hex = (component: number): string => {
const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16);
@@ -130,26 +131,43 @@ export const rgb2hs = (rgb: [number, number, number]): [number, number] =>
export const hs2rgb = (hs: [number, number]): [number, number, number] =>
hsv2rgb([hs[0], hs[1], 255]);
export function theme2hex(themeColor: string): string {
if (themeColor.startsWith("#")) {
if (themeColor.length === 4 || themeColor.length === 5) {
const c = themeColor;
/**
* Attempt to get a HEX color from a color defined in different formats:
* HEX, rgb/rgba, named color
* @param color - Color (HEX, rgb/rgba, named color) to be converted to HEX
* @returns HEX color
*/
export function theme2hex(color: string): string {
// Attempting to find a HEX pattern in the input string
if (color.startsWith("#")) {
if (color.length === 4 || color.length === 5) {
const c = color;
// Convert short-form hex (#abc) to 6 digit (#aabbcc). Ignore alpha channel.
return `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`;
}
if (themeColor.length === 9) {
if (color.length === 9) {
// Ignore alpha channel.
return themeColor.substring(0, 7);
return color.substring(0, 7);
}
return themeColor;
return color;
}
const rgbFromColorName = colors[themeColor.toLowerCase()];
if (rgbFromColorName) {
return rgb2hex(rgbFromColorName);
// Attempting to find a match in a HA Frontend theme colors
const themeColor = resolveThemeColor(color.toLowerCase());
if (themeColor !== color.toLowerCase()) {
// theme color is recognized, now re-attempt
return theme2hex(themeColor);
}
const rgbMatch = themeColor.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
// Attempting to find a match in a web colors array
const rgbFromWebColor = colors[color.toLowerCase()];
if (rgbFromWebColor) {
// HEX color is recognized for the input named color
return rgb2hex(rgbFromWebColor);
}
// Attempting to find an RGB pattern in the input string
const rgbMatch = color.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (rgbMatch) {
const [, r, g, b] = rgbMatch.map(Number);
return rgb2hex([r, g, b]);
@@ -158,5 +176,5 @@ export function theme2hex(themeColor: string): string {
// We have a named color, and there's nothing in the table,
// so nothing further we can do with it.
// Compare/border/background color will all be the same.
return themeColor;
return color;
}
+11
View File
@@ -1,4 +1,5 @@
import { wcagLuminance, wcagContrast } from "culori";
import { theme2hex } from "./convert-color";
/**
* Calculates the luminosity of an RGB color.
@@ -48,3 +49,13 @@ export const getRGBContrastRatio = (
rgb1: [number, number, number],
rgb2: [number, number, number]
) => Math.round((rgbContrast(rgb1, rgb2) + Number.EPSILON) * 100) / 100;
/**
* Returns a contrasted color (black or white) based on the luminance of another color
* @param color - Color (HEX, rgb/rgba, named color) to calculate a contrasted color
* @returns HEX color ("#000000" for dark backgrounds, "#ffffff" for light backgrounds)
*/
export const getContrastedColorHex = (color: string): string => {
const lum = wcagLuminance(theme2hex(color));
return lum > 0.5 ? "#000000" : "#ffffff";
};
+29
View File
@@ -5,12 +5,41 @@ import {
formatDateMonthYear,
formatDateVeryShort,
formatDateWeekdayShort,
formatDateYear,
} from "../../common/datetime/format_date";
import {
formatTime,
formatTimeWithSeconds,
} from "../../common/datetime/format_time";
export function getPeriodicAxisLabelConfig(
period: string,
locale: FrontendLocaleData,
config: HassConfig
):
| {
formatter: (value: number) => string;
}
| undefined {
if (period === "month") {
return {
formatter: (value: number) => {
const date = new Date(value);
return date.getMonth() === 0
? `{bold|${formatDateMonthYear(date, locale, config)}}`
: formatDateMonth(date, locale, config);
},
};
}
if (period === "year") {
return {
formatter: (value: number) =>
formatDateYear(new Date(value), locale, config),
};
}
return undefined;
}
export function formatTimeLabel(
value: number | Date,
locale: FrontendLocaleData,
+122 -8
View File
@@ -91,6 +91,10 @@ export class HaChartBase extends LitElement {
private _lastTapTime?: number;
private _longPressTimer?: ReturnType<typeof setTimeout>;
private _longPressTriggered = false;
private _shouldResizeChart = false;
private _resizeAnimationDuration?: number;
@@ -128,6 +132,7 @@ export class HaChartBase extends LitElement {
public disconnectedCallback() {
super.disconnectedCallback();
this._legendPointerCancel();
this._pendingSetup = false;
while (this._listeners.length) {
this._listeners.pop()!();
@@ -302,22 +307,31 @@ export class HaChartBase extends LitElement {
`;
}
private _renderLegend() {
private _getLegendItems() {
if (!this.options?.legend || !this.data) {
return nothing;
return undefined;
}
const legend = ensureArray(this.options.legend).find(
(l) => l.show && l.type === "custom"
) as CustomLegendOption | undefined;
if (!legend) {
return nothing;
return undefined;
}
const datasets = ensureArray(this.data);
const items =
return (
legend.data ||
datasets
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
.map((d) => ({ id: d.id, name: d.name }));
.map((d) => ({ id: d.id, name: d.name }))
);
}
private _renderLegend() {
const items = this._getLegendItems();
if (!items) {
return nothing;
}
const datasets = ensureArray(this.data!);
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
@@ -362,6 +376,11 @@ export class HaChartBase extends LitElement {
return html`<li
.id=${id}
@click=${this._legendClick}
@pointerdown=${this._legendPointerDown}
@pointerup=${this._legendPointerCancel}
@pointerleave=${this._legendPointerCancel}
@pointercancel=${this._legendPointerCancel}
@contextmenu=${this._legendContextMenu}
class=${classMap({ hidden: this._hiddenDatasets.has(id) })}
.title=${name}
>
@@ -632,7 +651,7 @@ export class HaChartBase extends LitElement {
hideOverlap: true,
...axis.axisLabel,
},
minInterval,
minInterval: axis.minInterval ?? minInterval,
} as XAXisOption;
});
}
@@ -1022,11 +1041,52 @@ export class HaChartBase extends LitElement {
fireEvent(this, "chart-zoom", { start, end });
}
private _legendClick(ev: any) {
// Long-press to solo on touch/pen devices (500ms, consistent with action-handler-directive)
private _legendPointerDown(ev: PointerEvent) {
// Mouse uses Ctrl/Cmd+click instead
if (ev.pointerType === "mouse") {
return;
}
const id = (ev.currentTarget as HTMLElement)?.id;
if (!id) {
return;
}
this._longPressTriggered = false;
this._longPressTimer = setTimeout(() => {
this._longPressTriggered = true;
this._longPressTimer = undefined;
this._soloLegend(id);
}, 500);
}
private _legendPointerCancel() {
if (this._longPressTimer) {
clearTimeout(this._longPressTimer);
this._longPressTimer = undefined;
}
}
private _legendContextMenu(ev: Event) {
if (this._longPressTimer || this._longPressTriggered) {
ev.preventDefault();
}
}
private _legendClick(ev: MouseEvent) {
if (!this.chart) {
return;
}
const id = ev.currentTarget?.id;
if (this._longPressTriggered) {
this._longPressTriggered = false;
return;
}
const id = (ev.currentTarget as HTMLElement)?.id;
// Cmd+click on Mac (Ctrl+click is right-click there), Ctrl+click elsewhere
const soloModifier = isMac ? ev.metaKey : ev.ctrlKey;
if (soloModifier) {
this._soloLegend(id);
return;
}
if (this._hiddenDatasets.has(id)) {
this._getAllIdsFromLegend(this.options, id).forEach((i) =>
this._hiddenDatasets.delete(i)
@@ -1041,6 +1101,60 @@ export class HaChartBase extends LitElement {
this.requestUpdate("_hiddenDatasets");
}
private _soloLegend(id: string) {
const allIds = this._getAllLegendIds();
const clickedIds = this._getAllIdsFromLegend(this.options, id);
const otherIds = allIds.filter((i) => !clickedIds.includes(i));
const clickedIsOnlyVisible =
clickedIds.every((i) => !this._hiddenDatasets.has(i)) &&
otherIds.every((i) => this._hiddenDatasets.has(i));
if (clickedIsOnlyVisible) {
// Already solo'd on this item — restore all series to visible
for (const hiddenId of [...this._hiddenDatasets]) {
this._hiddenDatasets.delete(hiddenId);
fireEvent(this, "dataset-unhidden", { id: hiddenId });
}
} else {
// Solo: hide every other series, unhide clicked if it was hidden
for (const otherId of otherIds) {
if (!this._hiddenDatasets.has(otherId)) {
this._hiddenDatasets.add(otherId);
fireEvent(this, "dataset-hidden", { id: otherId });
}
}
for (const clickedId of clickedIds) {
if (this._hiddenDatasets.has(clickedId)) {
this._hiddenDatasets.delete(clickedId);
fireEvent(this, "dataset-unhidden", { id: clickedId });
}
}
}
this.requestUpdate("_hiddenDatasets");
}
private _getAllLegendIds(): string[] {
const items = this._getLegendItems();
if (!items) {
return [];
}
const allIds = new Set<string>();
for (const item of items) {
const primaryId =
typeof item === "string"
? item
: ((item.id as string) ?? (item.name as string) ?? "");
for (const expandedId of this._getAllIdsFromLegend(
this.options,
primaryId
)) {
allIds.add(expandedId);
}
}
return [...allIds];
}
private _toggleExpandedLegend() {
this.expandLegend = !this.expandLegend;
setTimeout(() => {
+12 -2
View File
@@ -65,6 +65,8 @@ export interface NetworkData {
categories?: { name: string; symbol: string }[];
}
const PHYSICS_DISABLE_THRESHOLD = 512;
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
let GraphChart: typeof import("echarts/lib/chart/graph/install");
@@ -94,7 +96,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
@state() private _reducedMotion = false;
@state() private _physicsEnabled = true;
@state() private _physicsEnabled?: boolean;
@state() private _showLabels = true;
@@ -122,6 +124,14 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
];
}
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (this._physicsEnabled === undefined && this.data?.nodes?.length > 1) {
this._physicsEnabled =
this.data.nodes.length <= PHYSICS_DISABLE_THRESHOLD;
}
}
protected render() {
if (!GraphChart || !this.data.nodes?.length) {
return nothing;
@@ -138,7 +148,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.data=${this._getSeries(
this.data,
this._physicsEnabled,
this._physicsEnabled ?? false,
this._reducedMotion,
this._showLabels,
isMobile,
+17
View File
@@ -32,6 +32,7 @@ import {
} from "../../data/recorder";
import type { ECOption } from "../../resources/echarts/echarts";
import type { HomeAssistant } from "../../types";
import { getPeriodicAxisLabelConfig } from "./axis-label";
import type { CustomLegendOption } from "./ha-chart-base";
import "./ha-chart-base";
@@ -293,6 +294,22 @@ export class StatisticsChart extends LitElement {
type: "time",
min: startTime,
max: this.endTime,
...(this.period === "month" && {
minInterval: 28 * 24 * 3600 * 1000,
axisLabel: getPeriodicAxisLabelConfig(
"month",
this.hass.locale,
this.hass.config
),
}),
...(this.period === "year" && {
minInterval: 365 * 24 * 3600 * 1000,
axisLabel: getPeriodicAxisLabelConfig(
"year",
this.hass.locale,
this.hass.config
),
}),
},
{
id: "hiddenAxis",
@@ -2,7 +2,6 @@ import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { computeCssColor } from "../../common/color/compute-color";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { stringCompare } from "../../common/string/compare";
@@ -53,16 +52,15 @@ class HaDataTableLabels extends LitElement {
}
private _renderLabel(label: LabelRegistryEntry, clickAction: boolean) {
const color = label?.color ? computeCssColor(label.color) : undefined;
return html`
<ha-label
dense
role="button"
tabindex="0"
.color=${label.color}
.item=${label}
@click=${clickAction ? this._labelClicked : undefined}
@keydown=${clickAction ? this._labelClicked : undefined}
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label?.icon
@@ -102,10 +100,6 @@ class HaDataTableLabels extends LitElement {
position: fixed;
flex-wrap: nowrap;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
.plus {
--ha-label-background-color: transparent;
border: 1px solid var(--divider-color);
+38 -14
View File
@@ -4,7 +4,7 @@ import type { ActionDetail } from "@material/mwc-list";
import { mdiCalendarToday } from "@mdi/js";
import "cally";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, queryAll, state } from "lit/decorators";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import {
formatCallyDateRange,
@@ -29,6 +29,7 @@ import "../ha-list-item";
import "../ha-time-input";
import type { DateRangePickerRanges } from "./ha-date-range-picker";
import { datePickerStyles, dateRangePickerStyles } from "./styles";
import type { HaTimeInput } from "../ha-time-input";
@customElement("date-range-picker")
export class DateRangePicker extends LitElement {
@@ -69,6 +70,8 @@ export class DateRangePicker extends LitElement {
to: { hours: 23, minutes: 59 },
};
@queryAll("ha-time-input") private _timeInputs?: NodeListOf<HaTimeInput>;
public connectedCallback() {
super.connectedCallback();
@@ -153,6 +156,7 @@ export class DateRangePicker extends LitElement {
)}
id="from"
placeholder-labels
auto-validate
></ha-time-input>
<ha-time-input
.value=${`${this._timeValue.to.hours}:${this._timeValue.to.minutes}`}
@@ -163,6 +167,7 @@ export class DateRangePicker extends LitElement {
)}
id="to"
placeholder-labels
auto-validate
></ha-time-input>
</div>
`
@@ -200,6 +205,14 @@ export class DateRangePicker extends LitElement {
let endDate = new Date(`${dates[1]}T23:59:00`);
if (this.timePicker) {
const timeInputs = this._timeInputs;
if (
timeInputs &&
![...timeInputs].every((input) => input.reportValidity())
) {
// If we have time inputs, and they don't all report valid, don't save
return;
}
startDate.setHours(this._timeValue.from.hours);
startDate.setMinutes(this._timeValue.from.minutes);
endDate.setHours(this._timeValue.to.hours);
@@ -261,12 +274,6 @@ export class DateRangePicker extends LitElement {
const dateRange: [Date, Date] = Object.values(this.ranges!)[
ev.detail.index
];
this._dateValue = formatCallyDateRange(
dateRange[0],
dateRange[1],
this.locale,
this.hassConfig
);
fireEvent(this, "value-changed", {
value: {
startDate: dateRange[0],
@@ -281,7 +288,8 @@ export class DateRangePicker extends LitElement {
private _handleChangeTime(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
const time = ev.detail.value;
const type = (ev.target as HaBaseTimeInput).id;
const target = ev.target as HaBaseTimeInput;
const type = target.id;
if (time) {
if (!this._timeValue) {
this._timeValue = {
@@ -301,17 +309,39 @@ export class DateRangePicker extends LitElement {
css`
.picker {
display: flex;
flex-direction: row;
}
.date-range-ranges {
border-right: 1px solid var(--divider-color);
min-width: 140px;
flex: 0 1 30%;
}
.range {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
padding: var(--ha-space-3);
overflow-x: hidden;
}
@media only screen and (max-width: 460px) {
.picker {
flex-direction: column;
}
.date-range-ranges {
flex-basis: 180px;
border-bottom: 1px solid var(--divider-color);
border-right: none;
overflow-y: scroll;
}
.range {
flex-basis: fit-content;
}
}
.times {
@@ -326,12 +356,6 @@ export class DateRangePicker extends LitElement {
padding: var(--ha-space-2);
border-top: 1px solid var(--divider-color);
}
@media only screen and (max-width: 500px) {
.date-range-ranges {
max-width: 30%;
}
}
`,
];
}
-27
View File
@@ -80,33 +80,6 @@ export const datePickerStyles = css`
text-align: center;
margin-left: 48px;
}
@media only screen and (max-width: 500px) {
calendar-month {
min-height: calc(34px * 7);
}
calendar-month::part(day) {
font-size: var(--ha-font-size-s);
}
calendar-month::part(button) {
height: 26px;
width: 26px;
}
calendar-month::part(range-inner),
calendar-month::part(range-start),
calendar-month::part(range-end),
calendar-month::part(selected),
calendar-month::part(selected):hover {
height: 34px;
width: 34px;
}
.heading {
font-size: var(--ha-font-size-s);
}
.month-year {
margin-left: 40px;
}
}
`;
export const dateRangePickerStyles = css`
+5 -3
View File
@@ -137,7 +137,7 @@ export class HaBaseTimeInput extends LitElement {
@property({ attribute: "placeholder-labels", type: Boolean })
public placeholderLabels = false;
@queryAll("ha-input") private _inputs?: HaInput[];
@queryAll("ha-input") private _inputs?: NodeListOf<HaInput>;
static shadowRootOptions = {
...LitElement.shadowRootOptions,
@@ -145,7 +145,9 @@ export class HaBaseTimeInput extends LitElement {
};
public reportValidity(): boolean {
return this._inputs?.every((input) => input.reportValidity()) ?? true;
const inputs = this._inputs;
if (!inputs) return true;
return [...inputs].every((input) => input.reportValidity());
}
protected render(): TemplateResult {
@@ -399,7 +401,7 @@ export class HaBaseTimeInput extends LitElement {
.time-separator,
ha-icon-button {
background-color: var(--ha-color-fill-neutral-quiet-resting);
background-color: var(--ha-color-form-background);
color: var(--ha-color-text-secondary);
border-bottom: 1px solid var(--ha-color-border-neutral-loud);
box-sizing: border-box;
+3
View File
@@ -100,6 +100,9 @@ export class HaDropdown extends Dropdown {
#menu {
padding: var(--ha-space-1);
}
wa-popup::part(popup) {
z-index: 200;
}
`,
];
}
+4 -13
View File
@@ -6,7 +6,6 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import { navigate } from "../common/navigate";
import { stringCompare } from "../common/string/compare";
@@ -98,17 +97,14 @@ export class HaFilterLabels extends LitElement {
this.value
),
(label) => label.label_id,
(label) => {
const color = label.color
? computeCssColor(label.color)
: undefined;
return html`<ha-check-list-item
(label) =>
html`<ha-check-list-item
.value=${label.label_id}
.selected=${(this.value || []).includes(label.label_id)}
hasMeta
>
<ha-label
style=${color ? `--color: ${color}` : ""}
.color=${label.color}
.description=${label.description}
>
${label.icon
@@ -119,8 +115,7 @@ export class HaFilterLabels extends LitElement {
: nothing}
${label.name}
</ha-label>
</ha-check-list-item>`;
}
</ha-check-list-item>`
)}
</ha-list> `
: nothing}
@@ -253,10 +248,6 @@ export class HaFilterLabels extends LitElement {
.warning {
color: var(--error-color);
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
.add {
position: absolute;
bottom: 0;
+27 -5
View File
@@ -1,18 +1,44 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { computeCssColor } from "../common/color/compute-color";
import { getContrastedColorHex } from "../common/color/rgb";
import { uid } from "../common/util/uid";
import "./ha-tooltip";
/**
* Returns CSS styles for a label's background & icon/text
* @param color Label color defined in HEX format
* @returns CSS styles
*/
export const getLabelColorStyle = (labelColor: string | undefined | null) => {
const color = labelColor ? computeCssColor(labelColor) : undefined;
return color
? `--ha-label-background-color: ${color};
--primary-text-color: ${getContrastedColorHex(labelColor!)};`
: `--ha-label-background-color: rgba(var(--rgb-primary-text-color), 0.15);`;
};
@customElement("ha-label")
class HaLabel extends LitElement {
@property({ type: Boolean, reflect: true }) dense = false;
@property()
public color?: string;
@property({ attribute: "description" })
public description?: string;
private _elementId = "label-" + uid();
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
if (!changedProps.has("color")) {
return;
}
this.style.cssText = getLabelColorStyle(this.color);
}
protected render(): TemplateResult {
return html`
<ha-tooltip
@@ -36,10 +62,6 @@ class HaLabel extends LitElement {
:host {
--ha-label-text-color: var(--primary-text-color);
--ha-label-icon-color: var(--primary-text-color);
--ha-label-background-color: rgba(
var(--rgb-primary-text-color),
0.15
);
--ha-label-background-opacity: 1;
border: 1px solid var(--outline-color);
position: relative;
+11 -7
View File
@@ -6,7 +6,6 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import { labelsContext } from "../data/context";
@@ -17,6 +16,7 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./chips/ha-chip-set";
import "./chips/ha-input-chip";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import { getLabelColorStyle } from "./ha-label";
import "./ha-label-picker";
import type { HaLabelPicker } from "./ha-label-picker";
import "./ha-tooltip";
@@ -106,9 +106,14 @@ export class HaLabelsPicker extends LitElement {
labels?.find((label) => label.label_id === id) || {
label_id: id,
name: id,
color: "rgba(var(--rgb-primary-text-color), 0.15)",
}
)
.sort((a, b) => stringCompare(a?.name || "", b?.name || "", language))
.map((label) => ({
...label,
style: getLabelColorStyle(label.color),
}))
);
protected render(): TemplateResult {
@@ -135,9 +140,6 @@ export class HaLabelsPicker extends LitElement {
(label) => label?.label_id,
(label) => {
if (!label) return nothing;
const color = label.color
? computeCssColor(label.color)
: undefined;
const elementId = "label-" + label.label_id;
return html`
<ha-tooltip
@@ -154,7 +156,7 @@ export class HaLabelsPicker extends LitElement {
.disabled=${this.disabled}
.label=${label.name}
selected
style=${color ? `--color: ${color}` : ""}
style=${label.style}
>
${label.icon
? html`<ha-icon
@@ -239,8 +241,10 @@ export class HaLabelsPicker extends LitElement {
height: var(--ha-space-8);
}
ha-input-chip {
--md-input-chip-selected-container-color: var(--color, var(--grey-color));
--ha-input-chip-selected-container-opacity: 0.5;
--md-input-chip-selected-container-color: var(
--ha-label-background-color,
var(--grey-color)
);
--md-input-chip-selected-outline-width: 1px;
}
label {
+1 -4
View File
@@ -153,10 +153,7 @@ export class HaPickerField extends PickerMixin(LitElement) {
right: 0;
height: 1px;
width: 100%;
background-color: var(
--mdc-text-field-idle-line-color,
rgba(0, 0, 0, 0.42)
);
background-color: var(--ha-color-border-neutral-loud);
transform:
height 180ms ease-in-out,
background-color 180ms ease-in-out;
+310
View File
@@ -0,0 +1,310 @@
export const RETRO_THEME = {
// Sharp corners
"ha-border-radius-sm": "0",
"ha-border-radius-md": "0",
"ha-border-radius-lg": "0",
"ha-border-radius-xl": "0",
"ha-border-radius-2xl": "0",
"ha-border-radius-3xl": "0",
"ha-border-radius-4xl": "0",
"ha-border-radius-5xl": "0",
"ha-border-radius-6xl": "0",
"ha-border-radius-pill": "0",
"ha-border-radius-circle": "0",
// Fonts
"ha-font-family-body":
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
"ha-font-family-heading":
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
"ha-font-family-code": "'Courier New', Courier, monospace",
"ha-font-family-longform":
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
// No transparency
"ha-dialog-scrim-backdrop-filter": "none",
// Disable animations
"ha-animation-duration-fast": "1ms",
"ha-animation-duration-normal": "1ms",
"ha-animation-duration-slow": "1ms",
modes: {
light: {
// Base colors
"primary-color": "#000080",
"dark-primary-color": "#00006B",
"light-primary-color": "#4040C0",
"accent-color": "#000080",
"primary-text-color": "#000000",
"secondary-text-color": "#404040",
"text-primary-color": "#ffffff",
"text-light-primary-color": "#000000",
"disabled-text-color": "#808080",
// Backgrounds
"primary-background-color": "#C0C0C0",
"lovelace-background": "#008080",
"secondary-background-color": "#C0C0C0",
"card-background-color": "#C0C0C0",
"clear-background-color": "#C0C0C0",
// RGB values
"rgb-primary-color": "0, 0, 128",
"rgb-accent-color": "0, 0, 128",
"rgb-primary-text-color": "0, 0, 0",
"rgb-secondary-text-color": "64, 64, 64",
"rgb-text-primary-color": "255, 255, 255",
"rgb-card-background-color": "192, 192, 192",
// UI chrome
"divider-color": "#808080",
"outline-color": "#808080",
"outline-hover-color": "#404040",
"shadow-color": "rgba(0, 0, 0, 0.5)",
"scrollbar-thumb-color": "#808080",
"disabled-color": "#808080",
// Cards - retro bevel effect
"ha-card-border-width": "1px",
"ha-card-border-color": "#808080",
"ha-card-box-shadow": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
"ha-card-border-radius": "0",
// Dialogs
"ha-dialog-border-radius": "0",
"ha-dialog-surface-background": "#C0C0C0",
"dialog-box-shadow": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
// Box shadows - retro bevel
"ha-box-shadow-s": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
"ha-box-shadow-m": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
"ha-box-shadow-l": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
// Header
"app-header-background-color": "#000080",
"app-header-text-color": "#ffffff",
"app-header-border-bottom": "2px outset #C0C0C0",
// Sidebar
"sidebar-background-color": "#C0C0C0",
"sidebar-text-color": "#000000",
"sidebar-selected-text-color": "#ffffff",
"sidebar-selected-icon-color": "#000080",
"sidebar-icon-color": "#000000",
// Input
"input-fill-color": "#C0C0C0",
"input-disabled-fill-color": "#C0C0C0",
"input-ink-color": "#000000",
"input-label-ink-color": "#000000",
"input-disabled-ink-color": "#808080",
"input-idle-line-color": "#808080",
"input-hover-line-color": "#000000",
"input-disabled-line-color": "#808080",
"input-outlined-idle-border-color": "#808080",
"input-outlined-hover-border-color": "#000000",
"input-outlined-disabled-border-color": "#C0C0C0",
"input-dropdown-icon-color": "#000000",
// Status colors
"error-color": "#FF0000",
"warning-color": "#FF8000",
"success-color": "#008000",
"info-color": "#000080",
// State
"state-icon-color": "#000080",
"state-active-color": "#000080",
"state-inactive-color": "#808080",
// Data table
"data-table-border-width": "0",
// Primary scale
"ha-color-primary-05": "#00003A",
"ha-color-primary-10": "#000050",
"ha-color-primary-20": "#000066",
"ha-color-primary-30": "#00007A",
"ha-color-primary-40": "#000080",
"ha-color-primary-50": "#0000AA",
"ha-color-primary-60": "#4040C0",
"ha-color-primary-70": "#6060D0",
"ha-color-primary-80": "#8080E0",
"ha-color-primary-90": "#C8C8D8",
"ha-color-primary-95": "#D8D8E0",
// Neutral scale
"ha-color-neutral-05": "#000000",
"ha-color-neutral-10": "#2A2A2A",
"ha-color-neutral-20": "#404040",
"ha-color-neutral-30": "#606060",
"ha-color-neutral-40": "#707070",
"ha-color-neutral-50": "#808080",
"ha-color-neutral-60": "#909090",
"ha-color-neutral-70": "#A0A0A0",
"ha-color-neutral-80": "#B0B0B0",
"ha-color-neutral-90": "#C8C8C8",
"ha-color-neutral-95": "#D0D0D0",
// Codemirror
"codemirror-keyword": "#000080",
"codemirror-operator": "#000000",
"codemirror-variable": "#008080",
"codemirror-variable-2": "#000080",
"codemirror-variable-3": "#808000",
"codemirror-builtin": "#800080",
"codemirror-atom": "#008080",
"codemirror-number": "#FF0000",
"codemirror-def": "#000080",
"codemirror-string": "#008000",
"codemirror-string-2": "#808000",
"codemirror-comment": "#808080",
"codemirror-tag": "#800000",
"codemirror-meta": "#000080",
"codemirror-attribute": "#FF0000",
"codemirror-property": "#000080",
"codemirror-qualifier": "#808000",
"codemirror-type": "#000080",
},
dark: {
// Base colors
"primary-color": "#4040C0",
"dark-primary-color": "#000080",
"light-primary-color": "#6060D0",
"accent-color": "#4040C0",
"primary-text-color": "#C0C0C0",
"secondary-text-color": "#A0A0A0",
"text-primary-color": "#ffffff",
"text-light-primary-color": "#C0C0C0",
"disabled-text-color": "#606060",
// Backgrounds
"primary-background-color": "#2A2A2A",
"lovelace-background": "#003030",
"secondary-background-color": "#2A2A2A",
"card-background-color": "#3A3A3A",
"clear-background-color": "#2A2A2A",
// RGB values
"rgb-primary-color": "64, 64, 192",
"rgb-accent-color": "64, 64, 192",
"rgb-primary-text-color": "192, 192, 192",
"rgb-secondary-text-color": "160, 160, 160",
"rgb-text-primary-color": "255, 255, 255",
"rgb-card-background-color": "58, 58, 58",
// UI chrome
"divider-color": "#606060",
"outline-color": "#606060",
"outline-hover-color": "#808080",
"shadow-color": "rgba(0, 0, 0, 0.7)",
"scrollbar-thumb-color": "#606060",
"disabled-color": "#606060",
// Cards - retro bevel effect
"ha-card-border-width": "1px",
"ha-card-border-color": "#606060",
"ha-card-box-shadow": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
"ha-card-border-radius": "0",
// Dialogs
"ha-dialog-border-radius": "0",
"ha-dialog-surface-background": "#3A3A3A",
"dialog-box-shadow": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
// Box shadows - retro bevel
"ha-box-shadow-s": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
"ha-box-shadow-m": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
"ha-box-shadow-l": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
// Header
"app-header-background-color": "#000060",
"app-header-text-color": "#ffffff",
"app-header-border-bottom": "2px outset #3A3A3A",
// Sidebar
"sidebar-background-color": "#2A2A2A",
"sidebar-text-color": "#C0C0C0",
"sidebar-selected-text-color": "#ffffff",
"sidebar-selected-icon-color": "#4040C0",
"sidebar-icon-color": "#A0A0A0",
// Input
"input-fill-color": "#3A3A3A",
"input-disabled-fill-color": "#3A3A3A",
"input-ink-color": "#C0C0C0",
"input-label-ink-color": "#A0A0A0",
"input-disabled-ink-color": "#606060",
"input-idle-line-color": "#606060",
"input-hover-line-color": "#808080",
"input-disabled-line-color": "#404040",
"input-outlined-idle-border-color": "#606060",
"input-outlined-hover-border-color": "#808080",
"input-outlined-disabled-border-color": "#404040",
"input-dropdown-icon-color": "#A0A0A0",
// Status colors
"error-color": "#FF4040",
"warning-color": "#FFA040",
"success-color": "#40C040",
"info-color": "#4040C0",
// State
"state-icon-color": "#4040C0",
"state-active-color": "#4040C0",
"state-inactive-color": "#606060",
// Data table
"data-table-border-width": "0",
// Primary scale
"ha-color-primary-05": "#00002A",
"ha-color-primary-10": "#000040",
"ha-color-primary-20": "#000060",
"ha-color-primary-30": "#000080",
"ha-color-primary-40": "#4040C0",
"ha-color-primary-50": "#6060D0",
"ha-color-primary-60": "#8080E0",
"ha-color-primary-70": "#A0A0F0",
"ha-color-primary-80": "#C0C0FF",
"ha-color-primary-90": "#3A3A58",
"ha-color-primary-95": "#303048",
// Neutral scale
"ha-color-neutral-05": "#1A1A1A",
"ha-color-neutral-10": "#2A2A2A",
"ha-color-neutral-20": "#3A3A3A",
"ha-color-neutral-30": "#4A4A4A",
"ha-color-neutral-40": "#606060",
"ha-color-neutral-50": "#707070",
"ha-color-neutral-60": "#808080",
"ha-color-neutral-70": "#909090",
"ha-color-neutral-80": "#A0A0A0",
"ha-color-neutral-90": "#C0C0C0",
"ha-color-neutral-95": "#D0D0D0",
// Codemirror
"codemirror-keyword": "#8080E0",
"codemirror-operator": "#C0C0C0",
"codemirror-variable": "#40C0C0",
"codemirror-variable-2": "#8080E0",
"codemirror-variable-3": "#C0C040",
"codemirror-builtin": "#C040C0",
"codemirror-atom": "#40C0C0",
"codemirror-number": "#FF6060",
"codemirror-def": "#8080E0",
"codemirror-string": "#40C040",
"codemirror-string-2": "#C0C040",
"codemirror-comment": "#808080",
"codemirror-tag": "#C04040",
"codemirror-meta": "#8080E0",
"codemirror-attribute": "#FF6060",
"codemirror-property": "#8080E0",
"codemirror-qualifier": "#C0C040",
"codemirror-type": "#8080E0",
"map-filter":
"invert(0.9) hue-rotate(170deg) brightness(1.5) contrast(1.2) saturate(0.3)",
},
},
};
+683
View File
@@ -0,0 +1,683 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
applyThemesOnElement,
invalidateThemeCache,
} from "../common/dom/apply_themes_on_element";
import type { LocalizeKeys } from "../common/translations/localize";
import { subscribeLabFeature } from "../data/labs";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import type { HomeAssistant } from "../types";
import { RETRO_THEME } from "./ha-retro-theme";
const TIP_COUNT = 25;
type CasitaExpression =
| "hi"
| "ok-nabu"
| "heart"
| "sleep"
| "great-job"
| "error";
const STORAGE_KEY = "retro-position";
const DRAG_THRESHOLD = 5;
const BUBBLE_TIMEOUT = 8000;
const SLEEP_TIMEOUT = 30000;
const BSOD_CLICK_COUNT = 5;
const BSOD_CLICK_TIMEOUT = 3000;
const BSOD_DISMISS_DELAY = 500;
@customElement("ha-retro")
export class HaRetro extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@state() private _enabled = false;
public hassSubscribe() {
return [
subscribeLabFeature(
this.hass!.connection,
"frontend",
"retro",
(feature) => {
this._enabled = feature.enabled;
}
),
];
}
@state() private _casitaVisible = true;
@state() private _showBubble = false;
@state() private _bubbleText = "";
@state() private _expression: CasitaExpression = "hi";
@state() private _position: { x: number; y: number } | null = null;
@state() private _showBsod = false;
private _clickCount = 0;
private _clickTimer?: ReturnType<typeof setTimeout>;
private _dragging = false;
private _dragStartX = 0;
private _dragStartY = 0;
private _dragOffsetX = 0;
private _dragOffsetY = 0;
private _dragMoved = false;
private _bubbleTimer?: ReturnType<typeof setTimeout>;
private _sleepTimer?: ReturnType<typeof setTimeout>;
private _boundPointerMove = this._onPointerMove.bind(this);
private _boundPointerUp = this._onPointerUp.bind(this);
private _themeApplied = false;
private _isApplyingTheme = false;
private _themeObserver?: MutationObserver;
connectedCallback(): void {
super.connectedCallback();
this._loadPosition();
this._resetSleepTimer();
this._applyRetroTheme();
this._startThemeObserver();
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._clearTimers();
this._stopThemeObserver();
this._revertTheme();
document.removeEventListener("pointermove", this._boundPointerMove);
document.removeEventListener("pointerup", this._boundPointerUp);
document.removeEventListener("keydown", this._boundDismissBsod);
}
protected willUpdate(changedProps: Map<string, unknown>): void {
if (changedProps.has("_enabled")) {
if (this._enabled) {
this.hass!.loadFragmentTranslation("retro");
this._applyRetroTheme();
this._startThemeObserver();
} else {
this._stopThemeObserver();
this._revertTheme();
}
}
if (changedProps.has("hass") && this._enabled) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
// Re-apply if darkMode changed
if (oldHass && oldHass.themes.darkMode !== this.hass!.themes.darkMode) {
this._themeApplied = false;
this._applyRetroTheme();
}
}
}
private _startThemeObserver(): void {
if (this._themeObserver) return;
this._themeObserver = new MutationObserver(() => {
if (this._isApplyingTheme || !this._enabled || !this.hass) return;
// Check if our theme was overwritten by the themes mixin
const el = document.documentElement as HTMLElement & {
__themes?: { cacheKey?: string };
};
if (!el.__themes?.cacheKey?.startsWith("Retro")) {
this._themeApplied = false;
this._applyRetroTheme();
}
});
this._themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["style"],
});
}
private _stopThemeObserver(): void {
this._themeObserver?.disconnect();
this._themeObserver = undefined;
}
private _applyRetroTheme(): void {
if (!this.hass || this._themeApplied) return;
this._isApplyingTheme = true;
const themes = {
...this.hass.themes,
themes: {
...this.hass.themes.themes,
Retro: RETRO_THEME,
},
};
invalidateThemeCache();
applyThemesOnElement(
document.documentElement,
themes,
"Retro",
{ dark: this.hass.themes.darkMode },
true
);
this._themeApplied = true;
this._isApplyingTheme = false;
}
private _revertTheme(): void {
if (!this.hass || !this._themeApplied) return;
this._isApplyingTheme = true;
invalidateThemeCache();
applyThemesOnElement(
document.documentElement,
this.hass.themes,
this.hass.selectedTheme?.theme || "default",
{
dark: this.hass.themes.darkMode,
primaryColor: this.hass.selectedTheme?.primaryColor,
accentColor: this.hass.selectedTheme?.accentColor,
},
true
);
this._themeApplied = false;
this._isApplyingTheme = false;
}
private _loadPosition(): void {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const pos = JSON.parse(stored);
if (typeof pos.x === "number" && typeof pos.y === "number") {
this._position = this._clampPosition(pos.x, pos.y);
}
}
} catch {
// Ignore invalid stored position
}
}
private _savePosition(): void {
if (this._position) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this._position));
} catch {
// Ignore storage errors
}
}
}
private _clampPosition(x: number, y: number): { x: number; y: number } {
const size = 80;
return {
x: Math.max(0, Math.min(window.innerWidth - size, x)),
y: Math.max(0, Math.min(window.innerHeight - size, y)),
};
}
private _onPointerDown(ev: PointerEvent): void {
if (ev.button !== 0 || this._showBsod) return;
this._dragging = true;
this._dragMoved = false;
this._dragStartX = ev.clientX;
this._dragStartY = ev.clientY;
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
this._dragOffsetX = ev.clientX - rect.left;
this._dragOffsetY = ev.clientY - rect.top;
(ev.currentTarget as HTMLElement).setPointerCapture(ev.pointerId);
document.addEventListener("pointermove", this._boundPointerMove);
document.addEventListener("pointerup", this._boundPointerUp);
ev.preventDefault();
}
private _onPointerMove(ev: PointerEvent): void {
if (!this._dragging) return;
const dx = ev.clientX - this._dragStartX;
const dy = ev.clientY - this._dragStartY;
if (!this._dragMoved && Math.hypot(dx, dy) < DRAG_THRESHOLD) {
return;
}
this._dragMoved = true;
const x = ev.clientX - this._dragOffsetX;
const y = ev.clientY - this._dragOffsetY;
this._position = this._clampPosition(x, y);
}
private _onPointerUp(ev: PointerEvent): void {
document.removeEventListener("pointermove", this._boundPointerMove);
document.removeEventListener("pointerup", this._boundPointerUp);
this._dragging = false;
if (this._dragMoved) {
this._savePosition();
} else {
this._toggleBubble();
}
ev.preventDefault();
}
private _stopPropagation(ev: Event): void {
ev.stopPropagation();
}
private _dismiss(ev: Event): void {
ev.stopPropagation();
this._casitaVisible = false;
this._clearTimers();
}
private _toggleBubble(): void {
this._clickCount++;
if (this._clickTimer) {
clearTimeout(this._clickTimer);
}
this._clickTimer = setTimeout(() => {
this._clickCount = 0;
}, BSOD_CLICK_TIMEOUT);
if (this._clickCount >= BSOD_CLICK_COUNT) {
this._clickCount = 0;
this._triggerBsod();
return;
}
if (this._showBubble) {
this._hideBubble();
} else {
this._showTip();
}
}
private _boundDismissBsod = this._dismissBsodOnKey.bind(this);
private _bsodReadyToDismiss = false;
private _triggerBsod(): void {
this._hideBubble();
this._showBsod = true;
this._bsodReadyToDismiss = false;
this._expression = "error";
// Delay enabling dismiss so the rapid clicks that triggered the BSOD don't immediately close it
setTimeout(() => {
this._bsodReadyToDismiss = true;
document.addEventListener("keydown", this._boundDismissBsod);
}, BSOD_DISMISS_DELAY);
}
private _dismissBsod(): void {
if (!this._bsodReadyToDismiss) return;
this._showBsod = false;
this._expression = "hi";
this._resetSleepTimer();
document.removeEventListener("keydown", this._boundDismissBsod);
}
private _dismissBsodOnKey(): void {
this._dismissBsod();
}
private _showTip(): void {
const tipIndex = Math.floor(Math.random() * TIP_COUNT) + 1;
this._bubbleText = this.hass!.localize(
`ui.panel.retro.tip_${tipIndex}` as LocalizeKeys
);
this._showBubble = true;
this._expression = "ok-nabu";
this._resetSleepTimer();
if (this._bubbleTimer) {
clearTimeout(this._bubbleTimer);
}
this._bubbleTimer = setTimeout(() => {
this._hideBubble();
}, BUBBLE_TIMEOUT);
}
private _hideBubble(): void {
this._showBubble = false;
this._expression = "hi";
this._resetSleepTimer();
if (this._bubbleTimer) {
clearTimeout(this._bubbleTimer);
this._bubbleTimer = undefined;
}
}
private _closeBubble(ev: Event): void {
ev.stopPropagation();
this._hideBubble();
}
private _resetSleepTimer(): void {
if (this._sleepTimer) {
clearTimeout(this._sleepTimer);
}
this._sleepTimer = setTimeout(() => {
if (!this._showBubble) {
this._expression = "sleep";
}
}, SLEEP_TIMEOUT);
}
private _clearTimers(): void {
if (this._bubbleTimer) {
clearTimeout(this._bubbleTimer);
this._bubbleTimer = undefined;
}
if (this._sleepTimer) {
clearTimeout(this._sleepTimer);
this._sleepTimer = undefined;
}
if (this._clickTimer) {
clearTimeout(this._clickTimer);
this._clickTimer = undefined;
}
}
protected render() {
if (!this._enabled || !this._casitaVisible) {
return nothing;
}
const size = 80;
const posStyle = this._position
? `left: ${this._position.x}px; top: ${this._position.y}px;`
: `right: 16px; bottom: 16px;`;
return html`
${this._showBsod
? html`
<div class="bsod" @click=${this._dismissBsod}>
<div class="bsod-content">
<h1 class="bsod-title">
${this.hass!.localize("ui.panel.retro.bsod_title")}
</h1>
<p>${this.hass!.localize("ui.panel.retro.bsod_error")}</p>
<p>
* ${this.hass!.localize("ui.panel.retro.bsod_line_1")}<br />
* ${this.hass!.localize("ui.panel.retro.bsod_line_2")}
</p>
<p class="bsod-prompt">
${this.hass!.localize("ui.panel.retro.bsod_continue")}
<span class="bsod-cursor">_</span>
</p>
</div>
</div>
`
: nothing}
<div
class="casita-container ${this._dragging ? "dragging" : ""}"
style="width: ${size}px; ${posStyle}"
aria-hidden="true"
@pointerdown=${this._onPointerDown}
>
${this._showBubble
? html`
<div class="speech-bubble">
<span class="bubble-text">${this._bubbleText}</span>
<button
class="bubble-close"
@pointerdown=${this._stopPropagation}
@click=${this._closeBubble}
>
</button>
<button
class="bubble-dismiss"
@pointerdown=${this._stopPropagation}
@click=${this._dismiss}
>
${this.hass!.localize("ui.panel.retro.dismiss")}
</button>
<div class="bubble-arrow"></div>
</div>
`
: nothing}
<img
class="casita-image"
src="/static/images/voice-assistant/${this._expression}.png"
alt="Casita"
draggable="false"
/>
</div>
`;
}
static readonly styles = css`
:host {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
user-select: none;
z-index: 9999;
}
.casita-container {
position: fixed;
pointer-events: auto;
cursor: grab;
user-select: none;
touch-action: none;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
}
.casita-container.dragging {
cursor: grabbing;
}
.casita-image {
width: 100%;
height: auto;
animation: bob 3s ease-in-out infinite;
pointer-events: none;
}
.dragging .casita-image {
animation: none;
}
.speech-bubble {
position: absolute;
bottom: calc(100% + 8px);
right: 0;
background: #ffffe1;
color: #000000;
border-radius: 12px;
border: 2px solid #000000;
padding: 12px 28px 12px 12px;
font-family: Tahoma, "MS Sans Serif", Arial, sans-serif;
font-size: 14px;
line-height: 1.4;
width: 300px;
box-sizing: border-box;
word-wrap: break-word;
overflow-wrap: break-word;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
animation: bubble-in 200ms ease-out;
pointer-events: auto;
}
.bubble-close {
position: absolute;
top: 4px;
right: 4px;
background: none;
border: none;
cursor: pointer;
color: #000000;
font-size: 14px;
padding: 2px 6px;
line-height: 1;
border-radius: 50%;
}
.bubble-close:hover {
background: #e0e0c0;
}
.bubble-dismiss {
display: block;
margin-top: 8px;
background: none;
border: none;
cursor: pointer;
color: #808080;
font-family: Tahoma, "MS Sans Serif", Arial, sans-serif;
font-size: 12px;
padding: 0;
text-decoration: underline;
}
.bubble-dismiss:hover {
color: #000000;
}
.bubble-arrow {
position: absolute;
bottom: -8px;
right: 32px;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid #ffffe1;
}
@keyframes bob {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-4px);
}
}
@keyframes bubble-in {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.bsod {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #0000aa;
color: #ffffff;
font-family: "Lucida Console", "Courier New", monospace;
font-size: 16px;
line-height: 1.6;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
pointer-events: auto;
animation: bsod-in 100ms ease-out;
}
.bsod-content {
max-width: 700px;
padding: 32px;
text-align: left;
}
.bsod-title {
display: inline-block;
background: #aaaaaa;
color: #0000aa;
padding: 2px 12px;
font-size: 18px;
font-weight: normal;
margin: 0 0 24px;
}
.bsod-content p {
margin: 16px 0;
}
.bsod-prompt {
margin-top: 32px;
}
.bsod-cursor {
animation: blink 1s step-end infinite;
}
@keyframes bsod-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes blink {
50% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.casita-image {
animation: none;
}
.speech-bubble {
animation: none;
}
.bsod {
animation: none;
}
.bsod-cursor {
animation: none;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-retro": HaRetro;
}
}
@@ -287,7 +287,9 @@ export class HaNumericThresholdSelector extends LitElement {
const numberSelector = {
number: {
...this.selector.numeric_threshold?.number,
...(effectiveUnit ? { unit_of_measurement: effectiveUnit } : {}),
...(!showUnit && effectiveUnit
? { unit_of_measurement: effectiveUnit }
: {}),
},
};
const entitySelector = {
+17
View File
@@ -21,6 +21,8 @@ export class HaTimeInput extends LitElement {
@property({ type: Boolean }) public required = false;
@property({ attribute: "auto-validate", type: Boolean }) autoValidate = false;
@property({ type: Boolean, attribute: "enable-second" })
public enableSecond = false;
@@ -71,6 +73,7 @@ export class HaTimeInput extends LitElement {
.clearable=${this.clearable && this.value !== undefined}
.helper=${this.helper}
.placeholderLabels=${this.placeholderLabels}
.autoValidate=${this.autoValidate}
day-label="dd"
hour-label="hh"
min-label="mm"
@@ -86,6 +89,7 @@ export class HaTimeInput extends LitElement {
const useAMPM = useAmPm(this.locale);
let value: string | undefined;
let updateHours = 0;
// An undefined eventValue means the time selector is being cleared,
// the `value` variable will (intentionally) be left undefined.
@@ -97,6 +101,8 @@ export class HaTimeInput extends LitElement {
) {
let hours = eventValue.hours || 0;
if (eventValue && useAMPM) {
updateHours =
hours >= 12 && hours < 24 ? hours - 12 : hours === 0 ? 12 : 0;
if (eventValue.amPm === "PM" && hours < 12) {
hours += 12;
}
@@ -115,6 +121,17 @@ export class HaTimeInput extends LitElement {
}`;
}
if (updateHours) {
// If the user entered a 24hr time in a 12hr input, we need to refresh the
// input to ensure it resets back to the 12hr equivalent.
this.updateComplete.then(() => {
const input = this._input;
if (input) {
input.hours = updateHours;
}
});
}
if (value === this.value) {
return;
}
+27 -19
View File
@@ -1,6 +1,12 @@
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import {
customElement,
property,
query,
queryAssignedElements,
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { popoverSupported } from "../common/feature-detect/support-popover";
@@ -25,6 +31,12 @@ export class HaToast extends LitElement {
@query(".toast")
private _toast?: HTMLDivElement;
@queryAssignedElements({ slot: "action", flatten: true })
private _actionElements?: Element[];
@queryAssignedElements({ slot: "dismiss", flatten: true })
private _dismissElements?: Element[];
@state() private _active = false;
@state() private _visible = false;
@@ -163,6 +175,10 @@ export class HaToast extends LitElement {
}
protected render() {
const hasAction =
(this._actionElements?.length ?? 0) > 0 ||
(this._dismissElements?.length ?? 0) > 0;
return html`
<div
class=${classMap({
@@ -175,7 +191,7 @@ export class HaToast extends LitElement {
popover=${ifDefined(popoverSupported ? "manual" : undefined)}
>
<span class="message">${this.labelText}</span>
<div class="actions">
<div class=${classMap({ actions: true, "has-action": hasAction })}>
<slot name="action"></slot>
<slot name="dismiss"></slot>
</div>
@@ -198,21 +214,13 @@ export class HaToast extends LitElement {
border: none;
overflow: hidden;
box-sizing: border-box;
min-width: min(
350px,
calc(
100vw - var(--ha-space-4) - var(--safe-area-inset-left, 0px) - var(
--safe-area-inset-right,
0px
)
)
);
max-width: 650px;
min-width: min(350px, calc(var(--safe-width) - var(--ha-space-4)));
max-width: min(650px, var(--safe-width));
min-height: 48px;
display: flex;
align-items: center;
gap: var(--ha-space-2);
padding: var(--ha-space-3);
padding: var(--ha-space-3) var(--ha-space-4);
color: var(--ha-color-on-neutral-loud);
background-color: var(--ha-color-neutral-10);
border-radius: var(--ha-border-radius-sm);
@@ -245,14 +253,14 @@ export class HaToast extends LitElement {
color: var(--ha-color-on-neutral-loud);
}
.actions:not(.has-action) {
display: none;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.toast {
min-width: calc(
100vw - var(--safe-area-inset-left, 0px) - var(
--safe-area-inset-right,
0px
)
);
min-width: var(--safe-width);
max-width: var(--safe-width);
border-radius: var(--ha-border-radius-square);
}
}
+1 -7
View File
@@ -1,13 +1,7 @@
import type { Connection } from "home-assistant-js-websocket";
import { createCollection } from "home-assistant-js-websocket";
export interface ThemeVars {
// Incomplete
"primary-color": string;
"text-primary-color": string;
"accent-color": string;
[key: string]: string;
}
export type ThemeVars = Record<string, string>;
export type Theme = ThemeVars & {
modes?: {
@@ -82,6 +82,7 @@ class MoreInfoInputDatetime extends LitElement {
display: flex;
align-items: center;
justify-content: flex-end;
--ha-input-padding-bottom: 0;
}
ha-date-input + ha-time-input {
margin-left: var(--ha-space-1);
+2
View File
@@ -52,6 +52,7 @@ export class HomeAssistantMain extends LitElement {
return html`
<ha-snowflakes .hass=${this.hass} .narrow=${this.narrow}></ha-snowflakes>
<ha-retro .hass=${this.hass} .narrow=${this.narrow}></ha-retro>
<ha-drawer
.type=${sidebarNarrow ? "modal" : ""}
.open=${sidebarNarrow ? this._drawerOpen : false}
@@ -79,6 +80,7 @@ export class HomeAssistantMain extends LitElement {
protected firstUpdated() {
import(/* webpackPreload: true */ "../components/ha-sidebar");
import("../components/ha-snowflakes");
import("../components/ha-retro");
if (this.hass.auth.external) {
this._externalSidebar =
@@ -1038,6 +1038,7 @@ class SupervisorAppInfo extends LitElement {
}
private _updateComplete() {
this._scheduleDataUpdate();
const eventdata = {
success: true,
response: undefined,
@@ -1059,11 +1060,16 @@ class SupervisorAppInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
showAlertDialog(this, {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.dashboard.action_error.install"
),
text: extractApiErrorMessage(err),
confirmText: this.hass.localize("ui.common.ok"),
dismissText: this.hass.localize(
"ui.panel.config.apps.dashboard.action_error.view_supervisor_logs"
),
cancel: () => navigate("/config/logs?provider=supervisor"),
});
}
button.progress = false;
@@ -198,7 +198,7 @@ export default class HaAutomationAction extends AutomationSortableListMixin<Acti
private _addAction = (action: string, target?: HassServiceTarget) => {
let actions: Action[];
if (action === PASTE_VALUE) {
actions = this.actions.concat(deepClone(this._clipboard!.action));
actions = this.actions.concat(deepClone(this._clipboard!.action!));
} else if (action in VIRTUAL_ACTIONS) {
actions = this.actions.concat(VIRTUAL_ACTIONS[action]);
} else if (isDynamic(action)) {
@@ -287,7 +287,7 @@ export default class HaAutomationCondition extends AutomationSortableListMixin<C
let conditions: Condition[];
if (value === PASTE_VALUE) {
conditions = this.conditions.concat(
deepClone(this._clipboard!.condition)
deepClone(this._clipboard!.condition!)
);
} else if (isDynamic(value)) {
conditions = this.conditions.concat({
@@ -25,7 +25,6 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
@@ -1322,7 +1321,6 @@ ${rejected
private _renderLabelItems = (slot = "") =>
html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
@@ -1342,10 +1340,7 @@ ${rejected
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
<ha-label .color=${label.color} .description=${label.description}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
@@ -1490,10 +1485,6 @@ ${rejected
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
`,
];
}
@@ -203,7 +203,7 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
private _addTrigger = (value: string, target?: HassServiceTarget) => {
let triggers: Trigger[];
if (value === PASTE_VALUE) {
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger!));
} else if (isDynamic(value)) {
triggers = this.triggers.concat({
trigger: getValueFromDynamic(value),
@@ -3,7 +3,6 @@ import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../../common/color/compute-color";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
import { stringCompare } from "../../../../common/string/compare";
@@ -172,13 +171,9 @@ export class HaDeviceCard extends LitElement {
<div class="extra-info labels">
${labels.map((labelId) => {
const label = labelMap.get(labelId);
const color =
label?.color && typeof label.color === "string"
? computeCssColor(label.color)
: undefined;
return html`
<ha-label
style=${color ? `--color: ${color}` : ""}
.color=${label?.color}
.description=${label?.description}
>
${label?.icon
@@ -243,12 +238,6 @@ export class HaDeviceCard extends LitElement {
max-width: 100%;
flex: 0 1 auto;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
--ha-label-text-color: var(--primary-text-color);
--ha-label-icon-color: var(--primary-text-color);
}
.extra-info {
margin-top: var(--ha-space-2);
word-wrap: break-word;
@@ -10,11 +10,9 @@ import {
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
@@ -36,7 +34,6 @@ import type {
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-alert";
@@ -681,7 +678,6 @@ export class HaConfigDeviceDashboard extends LitElement {
private _renderLabelItems = (slot = "") =>
html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((deviceId) =>
this.hass.devices[deviceId]?.labels.includes(label.label_id)
);
@@ -703,7 +699,7 @@ export class HaConfigDeviceDashboard extends LitElement {
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.color=${label.color}
.description=${label.description || undefined}
>
${label.icon
@@ -1254,10 +1250,6 @@ ${rejected
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
`,
haStyle,
];
@@ -23,7 +23,6 @@ import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoize from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeAreaName } from "../../../common/entity/compute_area_name";
@@ -754,7 +753,6 @@ export class HaConfigEntities extends LitElement {
private _renderLabelItems = (slot = "") =>
html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
@@ -774,10 +772,7 @@ export class HaConfigEntities extends LitElement {
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
<ha-label .color=${label.color} .description=${label.description}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
@@ -1678,10 +1673,6 @@ ${rejected
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
`,
];
}
+1 -10
View File
@@ -19,7 +19,6 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
@@ -1429,7 +1428,6 @@ ${rejected
private _renderLabelItems = (slot = "") =>
html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this._labelsForEntity(entityId).includes(label.label_id)
);
@@ -1449,10 +1447,7 @@ ${rejected
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
<ha-label .color=${label.color} .description=${label.description}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
@@ -1538,10 +1533,6 @@ ${rejected
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
`,
];
}
+8 -5
View File
@@ -46,11 +46,14 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
const featuresToSort = [...features];
return featuresToSort.sort((a, b) => {
// Place frontend.winter_mode at the bottom
if (a.domain === "frontend" && a.preview_feature === "winter_mode")
return 1;
if (b.domain === "frontend" && b.preview_feature === "winter_mode")
return -1;
// Place frontend fun features at the bottom
const funFeatures = ["winter_mode", "retro"];
const aIsFun =
a.domain === "frontend" && funFeatures.includes(a.preview_feature);
const bIsFun =
b.domain === "frontend" && funFeatures.includes(b.preview_feature);
if (aIsFun && !bIsFun) return 1;
if (bIsFun && !aIsFun) return -1;
// Sort everything else alphabetically
return domainToName(localize, a.domain).localeCompare(
+1 -10
View File
@@ -22,7 +22,6 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -1116,7 +1115,6 @@ ${rejected
private _renderLabelItems = (slot = "") =>
html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
@@ -1136,10 +1134,7 @@ ${rejected
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
<ha-label .color=${label.color} .description=${label.description}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
@@ -1272,10 +1267,6 @@ ${rejected
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
`,
];
}
+1 -10
View File
@@ -23,7 +23,6 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
@@ -1175,7 +1174,6 @@ ${rejected
private _renderLabelItems = (slot = "") =>
html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
@@ -1195,10 +1193,7 @@ ${rejected
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
<ha-label .color=${label.color} .description=${label.description}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
@@ -1330,10 +1325,6 @@ ${rejected
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
`,
];
}
@@ -35,6 +35,7 @@ import { formatTime } from "../../../../../common/datetime/format_time";
import type { ECOption } from "../../../../../resources/echarts/echarts";
import { filterXSS } from "../../../../../common/util/xss";
import type { StatisticPeriod } from "../../../../../data/recorder";
import { getPeriodicAxisLabelConfig } from "../../../../../components/chart/axis-label";
import { getSuggestedPeriod } from "../../../../../data/energy";
// Number of days of padding when showing time axis in months
@@ -109,17 +110,7 @@ export function getCommonOptions(
type: "time",
min: subDays(start, MONTH_TIME_AXIS_PADDING),
max: addDays(suggestedMax, MONTH_TIME_AXIS_PADDING),
axisLabel: {
formatter: {
year: "{yearStyle|{MMMM} {yyyy}}",
month: "{MMMM}",
},
rich: {
yearStyle: {
fontWeight: "bold",
},
},
},
axisLabel: getPeriodicAxisLabelConfig("month", locale, config),
// For shorter month ranges, force splitting to ensure time axis renders
// as whole month intervals. Limit the number of forced ticks to 6 months
// (so a max calendar difference of 5) to reduce clutter.
@@ -43,6 +43,7 @@ const COLORS: Record<HomeSummary, string> = {
security: "blue-grey",
media_players: "blue",
energy: "amber",
persons: "green",
};
@customElement("hui-home-summary-card")
@@ -257,6 +258,21 @@ export class HuiHomeSummaryCard
const totalConsumption = consumption.total.used_total;
return formatConsumptionShort(this.hass, totalConsumption, "kWh");
}
case "persons": {
const personsFilters = HOME_SUMMARIES_FILTERS.persons.map((filter) =>
generateEntityFilter(this.hass!, filter)
);
const personEntities = findEntities(allEntities, personsFilters);
const personsHome = personEntities.filter((entityId) => {
const s = this.hass!.states[entityId]?.state;
return s === "home";
});
return personsHome.length
? this.hass.localize("ui.card.home-summary.count_persons_home", {
count: personsHome.length,
})
: this.hass.localize("ui.card.home-summary.nobody_home");
}
}
return "";
}
+3 -12
View File
@@ -10,7 +10,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { getColorByIndex } from "../../../common/color/colors";
import { computeCssVariableName } from "../../../common/color/compute-color";
import { resolveThemeColor } from "../../../common/color/compute-color";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
@@ -431,15 +431,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return color;
}
private _resolveColor(color: string): string {
const cssColor = computeCssVariableName(color);
if (cssColor.startsWith("--")) {
const resolved = getComputedStyle(this).getPropertyValue(cssColor).trim();
return resolved || color;
}
return cssColor;
}
private _getSourceEntities(states?: HassEntities): GeoEntity[] {
if (!states || !this._config?.geo_location_sources) {
return [];
@@ -479,7 +470,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
...(this._configEntities || []).map((entityConf) => ({
entity_id: entityConf.entity,
color: entityConf.color
? this._resolveColor(entityConf.color)
? resolveThemeColor(entityConf.color)
: this._getColor(entityConf.entity),
label_mode: entityConf.label_mode,
attribute: entityConf.attribute,
@@ -541,7 +532,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
name,
fullDatetime: (config.hours_to_show ?? DEFAULT_HOURS_TO_SHOW) > 144,
color: entityConfig?.color
? this._resolveColor(entityConfig.color)
? resolveThemeColor(entityConfig.color)
: this._getColor(entityId),
gradualOpacity: 0.8,
});
@@ -19,8 +19,10 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import { ensureBadgeConfig } from "../../../data/lovelace/config/badge";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import {
ensureBadgeConfig,
type LovelaceBadgeConfig,
} from "../../../data/lovelace/config/badge";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
@@ -58,7 +60,7 @@ export class HuiBadgeEditMode extends LitElement {
subscribe: false,
storage: "sessionStorage",
})
protected _clipboard?: LovelaceCardConfig;
protected _clipboard?: string | Partial<LovelaceBadgeConfig>;
private get _badges() {
const containerPath = getLovelaceContainerPath(this.path!);
@@ -1,13 +1,23 @@
import { mdiDelete, mdiDragHorizontalVariant, mdiPencil } from "@mdi/js";
import "@home-assistant/webawesome/dist/components/divider/divider";
import {
mdiDelete,
mdiDotsVertical,
mdiDragHorizontalVariant,
mdiPencil,
mdiPlusCircleMultipleOutline,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { deleteSection } from "../editor/config-util";
import { deleteSection, duplicateSection } from "../editor/config-util";
import { findLovelaceContainer } from "../editor/lovelace-path";
import { showEditSectionDialog } from "../editor/section-editor/show-edit-section-dialog";
import type { Lovelace } from "../types";
@@ -31,16 +41,32 @@ export class HuiSectionEditMode extends LitElement {
class="handle"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
<ha-icon-button
.label=${this.hass.localize("ui.common.edit")}
@click=${this._editSection}
.path=${mdiPencil}
></ha-icon-button>
<ha-icon-button
.label=${this.hass.localize("ui.common.delete")}
@click=${this._deleteSection}
.path=${mdiDelete}
></ha-icon-button>
<ha-dropdown
placement="bottom-end"
@wa-select=${this._handleDropdownSelect}
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="edit">
<ha-svg-icon slot="icon" .path=${mdiPencil}></ha-svg-icon>
${this.hass.localize("ui.common.edit")}
</ha-dropdown-item>
<ha-dropdown-item value="duplicate">
<ha-svg-icon
slot="icon"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
${this.hass.localize("ui.common.duplicate")}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="delete" variant="danger">
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
${this.hass.localize("ui.common.delete")}
</ha-dropdown-item>
</ha-dropdown>
</div>
</div>
<div class="section-wrapper">
@@ -49,8 +75,23 @@ export class HuiSectionEditMode extends LitElement {
`;
}
private async _editSection(ev) {
ev.stopPropagation();
private _handleDropdownSelect(ev: HaDropdownSelectEvent): void {
const action = ev.detail?.item?.value;
if (!action) return;
switch (action) {
case "edit":
this._editSection();
break;
case "duplicate":
this._duplicateSection();
break;
case "delete":
this._deleteSection();
break;
}
}
private async _editSection() {
showEditSectionDialog(this, {
lovelace: this.lovelace!,
lovelaceConfig: this.lovelace!.config,
@@ -62,8 +103,16 @@ export class HuiSectionEditMode extends LitElement {
});
}
private async _deleteSection(ev) {
ev.stopPropagation();
private _duplicateSection(): void {
const newConfig = duplicateSection(
this.lovelace!.config,
this.viewIndex,
this.index
);
this.lovelace!.saveConfig(newConfig);
}
private async _deleteSection() {
const path = [this.viewIndex, this.index] as [number, number];
const section = findLovelaceContainer(this.lovelace!.config, path);
@@ -153,7 +153,7 @@ export class HaCardConditionsEditor extends LitElement {
}
if (condition === "paste") {
const newCondition = deepClone(this._clipboard);
const newCondition = deepClone(this._clipboard!);
conditions.push(newCondition);
} else {
const elClass = customElements.get(`ha-card-condition-${condition}`) as
+14
View File
@@ -1,3 +1,4 @@
import deepClone from "deep-clone-simple";
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
import { ensureBadgeConfig } from "../../../data/lovelace/config/badge";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
@@ -303,6 +304,19 @@ export const deleteSection = (
return newConfig;
};
export const duplicateSection = (
config: LovelaceConfig,
viewIndex: number,
sectionIndex: number
): LovelaceConfig => {
const view = findLovelaceContainer(config, [viewIndex]);
if (isStrategyView(view)) {
throw new Error("Duplicating sections in a strategy is not supported.");
}
const clone = deepClone(view.sections![sectionIndex]);
return insertSection(config, viewIndex, sectionIndex + 1, clone);
};
export const insertSection = (
config: LovelaceConfig,
viewIndex: number,
@@ -112,7 +112,7 @@ export class HuiButtonHeadingBadge
width: auto;
height: var(--ha-heading-badge-size, 26px);
min-width: var(--ha-heading-badge-size, 26px);
font-size: var(--ha-heading-badge-font-size, var(--ha-font-size-m));
font-size: var(--ha-heading-badge-font-size, var(--ha-font-size-s));
font-weight: var(--ha-font-weight-medium);
}
ha-control-button.with-text {
@@ -135,6 +135,9 @@ export class HuiButtonHeadingBadge
padding: 0 var(--ha-space-1);
line-height: 1;
}
ha-icon {
line-height: 1;
}
`;
}
+30 -5
View File
@@ -110,11 +110,19 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
public disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener(
"card-visibility-changed",
this._cardVisibilityChanged
);
}
public connectedCallback() {
super.connectedCallback();
this._updateVisibility();
this.addEventListener(
"card-visibility-changed",
this._cardVisibilityChanged
);
}
protected update(changedProperties) {
@@ -144,7 +152,11 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
if (changedProperties.has("_cards")) {
this._layoutElement.cards = this._cards;
}
if (changedProperties.has("hass") || changedProperties.has("preview")) {
if (
changedProperties.has("hass") ||
changedProperties.has("preview") ||
changedProperties.has("_cards")
) {
this._updateVisibility();
}
}
@@ -200,6 +212,10 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
}
}
private _cardVisibilityChanged = () => {
this._updateVisibility();
};
protected _updateVisibility(conditionsMet?: boolean) {
if (!this._layoutElement || !this._config) {
return;
@@ -220,7 +236,16 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
(!this._config.visibility ||
checkConditionsMet(this._config.visibility, this.hass));
this._setElementVisibility(visible);
if (!visible) {
this._setElementVisibility(false);
return;
}
// Hide section when all cards are conditionally hidden
const allCardsHidden =
this._cards.length > 0 && this._cards.every((card) => card.hidden);
this._setElementVisibility(!allCardsHidden);
}
private _setElementVisibility(visible: boolean) {
@@ -232,9 +257,9 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
fireEvent(this, "section-visibility-changed", { value: visible });
}
if (!visible && this._layoutElement.parentElement) {
this.removeChild(this._layoutElement);
} else if (visible && !this._layoutElement.parentElement) {
// Always keep layout element connected so cards can still update
// their visibility and bubble events back to the section.
if (!this._layoutElement.parentElement) {
this.appendChild(this._layoutElement);
}
}
@@ -10,6 +10,7 @@ export const HOME_SUMMARIES = [
"security",
"media_players",
"energy",
"persons",
] as const;
export type HomeSummary = (typeof HOME_SUMMARIES)[number];
@@ -20,6 +21,7 @@ export const HOME_SUMMARIES_ICONS: Record<HomeSummary, string> = {
security: "mdi:security",
media_players: "mdi:multimedia",
energy: "mdi:lightning-bolt",
persons: "mdi:account-multiple",
};
export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
@@ -28,6 +30,7 @@ export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
security: securityEntityFilters,
media_players: [{ domain: "media_player", entity_category: "none" }],
energy: [], // Uses energy collection data
persons: [{ domain: "person" }],
};
export const getSummaryLabel = (
+57 -20
View File
@@ -216,7 +216,9 @@
"count_alarms_disarmed": "{count} {count, plural,\n one {disarmed}\n other {disarmed}\n}",
"all_secure": "All secure",
"no_media_playing": "No media playing",
"count_media_playing": "{count} {count, plural,\n one {playing}\n other {playing}\n}"
"count_media_playing": "{count} {count, plural,\n one {playing}\n other {playing}\n}",
"count_persons_home": "{count} {count, plural,\n one {person}\n other {people}\n}",
"nobody_home": "No one home"
},
"toggle-group": {
"all_off": "All off",
@@ -2866,7 +2868,8 @@
"get_changelog": "Failed to get changelog",
"start_invalid_config": "Invalid configuration",
"go_to_config": "Go to configuration",
"validate_config": "Failed to validate app configuration"
"validate_config": "Failed to validate app configuration",
"view_supervisor_logs": "View supervisor logs"
},
"uninstall_dialog": {
"title": "Uninstall {name}?",
@@ -5005,7 +5008,7 @@
"before": "Before",
"after": "After",
"description": {
"picker": "When a calendar event starts or ends.",
"picker": "Triggers when a calendar event starts or ends.",
"full": "When{offsetChoice, select, \n before { it's {offset} before}\n after { it's {offset} after}\n other {}\n} a calendar event{eventChoice, select, \n start { starts}\n end { ends}\n other { starts or ends}\n}{hasCalendar, select, \n true { in {calendar}}\n other {}\n}"
}
},
@@ -5013,7 +5016,7 @@
"label": "Device",
"trigger": "Trigger",
"description": {
"picker": "When something happens to a device. Great way to start."
"picker": "Triggers when something happens to a device. Great way to start."
}
},
"event": {
@@ -5024,7 +5027,7 @@
"context_user_picked": "User firing event",
"context_user_pick": "Select user",
"description": {
"picker": "When an event is being received (event is an advanced concept in Home Assistant).",
"picker": "Triggers when an event is being received (event is an advanced concept in Home Assistant).",
"full": "When {eventTypes} event is fired"
}
},
@@ -5036,7 +5039,7 @@
"enter": "Enter",
"leave": "Leave",
"description": {
"picker": "When an entity created by a geolocation platform appears in or disappears from a zone.",
"picker": "Triggers when an entity created by a geolocation platform appears in or disappears from a zone.",
"full": "When {source} {event, select, \n enter {enters}\n leave {leaves} other {} \n} {zone} {numberOfZones, plural,\n one {zone}\n other {zones}\n}"
}
},
@@ -5048,7 +5051,7 @@
"to": "To (optional)",
"any_state_ignore_attributes": "Any state (ignoring attribute changes)",
"description": {
"picker": "When the state of an entity (or attribute) changes.",
"picker": "Triggers when the state of an entity (or attribute) changes.",
"full": "When{hasAttribute, select, \n true { {attribute} of} \n other {}\n} {hasEntity, select, \n true {{entity}} \n other {something}\n} changes{fromChoice, select, \n fromUsed { from {fromString}}\n null { from any state} \n other {}\n}{toChoice, select, \n toUsed { to {toString}} \n null { to any state} \n special { state or any attributes} \n other {}\n}{hasDuration, select, \n true { for {duration}} \n other {}\n}"
}
},
@@ -5058,7 +5061,7 @@
"start": "Start",
"shutdown": "Shutdown",
"description": {
"picker": "When Home Assistant starts up or shuts down.",
"picker": "Triggers when Home Assistant starts up or shuts down.",
"started": "When Home Assistant is started",
"shutdown": "When Home Assistant is shut down"
}
@@ -5068,7 +5071,7 @@
"topic": "Topic",
"payload": "Payload (optional)",
"description": {
"picker": "When a specific message is received on a given MQTT topic.",
"picker": "Triggers when a specific message is received on a given MQTT topic.",
"full": "When an MQTT message has been received"
}
},
@@ -5082,7 +5085,7 @@
"type_value": "Fixed number",
"type_input": "Numeric value of another entity",
"description": {
"picker": "When the numeric value of an entity''s state (or attribute''s value) crosses a given threshold.",
"picker": "Triggers when the numeric value of an entity''s state (or attribute''s value) crosses a given threshold.",
"above": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} above {above}{duration, select, \n undefined {} \n other { for {duration}}\n }",
"below": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} below {below}{duration, select, \n undefined {} \n other { for {duration}}\n }",
"above-below": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} above {above} and below {below}{duration, select, \n undefined {} \n other { for {duration}}\n }"
@@ -5099,7 +5102,7 @@
"updated": "updated"
},
"description": {
"picker": "When a persistent notification is added or removed.",
"picker": "Triggers when a persistent notification is added or removed.",
"full": "When a persistent notification is updated"
}
},
@@ -5110,7 +5113,7 @@
"sunset": "Sunset",
"offset": "Offset in seconds or HH:MM:SS (optional)",
"description": {
"picker": "When the sun sets or rises.",
"picker": "Triggers when the sun sets or rises.",
"sets": "When the sun sets{hasDuration, select, \n true { offset by {duration}} \n other {}\n }",
"rises": "When the sun rises{hasDuration, select, \n true { offset by {duration}} \n other {}\n }"
}
@@ -5122,7 +5125,7 @@
"delete": "Delete sentence",
"confirm_delete": "Are you sure you want to delete this sentence?",
"description": {
"picker": "When Assist matches a sentence from a voice assistant.",
"picker": "Triggers when Assist matches a sentence from a voice assistant.",
"empty": "When a sentence is said",
"single": "When the sentence ''{sentence}'' is said",
"multiple": "When the sentence ''{sentence}'' or {count, plural,\n one {another}\n other {{count} others}\n} are said"
@@ -5131,7 +5134,7 @@
"tag": {
"label": "Tag",
"description": {
"picker": "When a tag is scanned (tags are usually created from the Companion app).",
"picker": "Triggers when a tag is scanned (tags are usually created from the Companion app).",
"full": "When a tag is scanned",
"known_tag": "When scanning tag {tag_name}"
}
@@ -5141,7 +5144,7 @@
"value_template": "Value template",
"for": "For",
"description": {
"picker": "When a template evaluates to true.",
"picker": "Triggers when a template evaluates to true.",
"full": "When a template changes from false to true{hasDuration, select, \n true { for {duration}} \n other {}\n }"
}
},
@@ -5165,7 +5168,7 @@
"sun": "[%key:ui::weekdays::sunday%]"
},
"description": {
"picker": "At a specific time, or on a specific date.",
"picker": "Triggers at a specific time, or on a specific date.",
"full": "When the time is equal to {time}{hasWeekdays, select, \n true { on {weekdays}} \n other {}\n}"
}
},
@@ -5176,7 +5179,7 @@
"minutes": "Minutes",
"seconds": "Seconds",
"description": {
"picker": "Periodically, at a defined interval.",
"picker": "Triggers periodically, at a defined interval.",
"initial": "When a time pattern matches",
"invalid": "Invalid time pattern for {parts}",
"full": "Trigger {secondsChoice, select, \n every {every second of }\n every_interval {every {seconds} seconds of }\n on_the_xth {on the {secondsWithOrdinal} second of }\n other {}\n} {minutesChoice, select, \n every {every minute of }\n every_interval {every {minutes} minutes of }\n has_seconds {the {minutesWithOrdinal} minute of }\n on_the_xth {on the {minutesWithOrdinal} minute of }\n other {}\n} {hoursChoice, select, \n every {every hour}\n every_interval {every {hours} hours}\n has_seconds_or_minutes {the {hoursWithOrdinal} hour}\n on_the_xth {on the {hoursWithOrdinal} hour}\n other {}\n}",
@@ -5191,7 +5194,7 @@
"webhook_id_helper": "Treat this ID like a password: keep it secret and make it hard to guess.",
"webhook_settings": "Webhook settings",
"description": {
"picker": "When Home Assistant receives a web request to the webhook endpoint.",
"picker": "Triggers when Home Assistant receives a web request to a webhook endpoint.",
"full": "When a Webhook payload has been received"
}
},
@@ -5203,7 +5206,7 @@
"enter": "Enter",
"leave": "Leave",
"description": {
"picker": "When someone (or something) enters or leaves a zone.",
"picker": "Triggers when someone (or something) enters or leaves a zone.",
"full": "When {entity} {event, select, \n enter {enters}\n leave {leaves} other {} \n} {zone} {numberOfZones, plural,\n one {zone} \n other {zones}\n}"
}
},
@@ -8217,7 +8220,8 @@
"media_players": "Media players",
"other_devices": "Other devices",
"weather": "Weather",
"energy": "Today's energy"
"energy": "Today's energy",
"persons": "People at home"
},
"welcome_user": "Welcome {user}",
"summaries": "Summaries",
@@ -10701,6 +10705,39 @@
"add_card": "Add current view as card",
"add_card_error": "Unable to add card",
"error_no_data": "You need to select some data sources first."
},
"retro": {
"tip_1": "Try turning your house off and on again.",
"tip_2": "If your automation doesn't work, just add more YAML.",
"tip_3": "Talk to your devices. They won't answer, but it helps.",
"tip_4": "The best way to secure your smart home is to go back to candles.",
"tip_5": "Rebooting fixes everything. Everything.",
"tip_6": "Naming your vacuum 'DJ Roomba' increases cleaning efficiency by 200%.",
"tip_7": "Your automations run better when you're not looking.",
"tip_8": "Every time you restart Home Assistant, a smart bulb loses its pairing.",
"tip_9": "The cloud is just someone else's Raspberry Pi.",
"tip_10": "You can automate your coffee machine, but you still have to drink it yourself.",
"tip_11": "You can save energy by not having a home.",
"tip_12": "Psst... you can drag me anywhere you want!",
"tip_13": "Did you know? I never sleep. Well, sometimes I do. Zzz...",
"tip_14": "Zigbee, Z-Wave, Wi-Fi, Thread... so many protocols, so little time.",
"tip_15": "The sun can trigger your automations. Nature is the best sensor.",
"tip_16": "It looks like you're trying to automate your home! Would you like help?",
"tip_17": "My previous job was a paperclip. I got promoted.",
"tip_18": "I run entirely on YAML and good vibes.",
"tip_19": "Somewhere, a smart plug is blinking and nobody knows why.",
"tip_20": "Home Assistant runs on a Raspberry Pi. I run on hopes and dreams.",
"tip_21": "Behind every great home, there's someone staring at logs at 2am.",
"tip_22": "404: Motivation not found. Try again after coffee.",
"tip_23": "There are two types of people: those who back up, and those who will.",
"tip_24": "My favorite color is #008080. Don't ask me why.",
"tip_25": "Automations are just spicy if-then statements.",
"dismiss": "Dismiss me",
"bsod_title": "Home Assistant",
"bsod_error": "A fatal exception 0E has occurred at C0FF:EE15G00D in VXD L1GHT5(01) + 0FF. The current automation will be terminated.",
"bsod_line_1": "Don't worry, nothing is actually broken.",
"bsod_line_2": "Your automations are still running. Probably.",
"bsod_continue": "Press any key or click to continue"
}
},
"tips": {
+3
View File
@@ -0,0 +1,3 @@
declare module "deep-clone-simple" {
export default function deepClone<T>(data: T): T;
}
+3
View File
@@ -47,8 +47,11 @@ describe("Color Conversion Tests", () => {
});
it("should convert theme color to hex (ignoring alpha)", () => {
// Warning: theme2hex("red") returns a value of `--red-color` variable
// which can differ from `#ff0000` on a particular Frontend client
expect(theme2hex("red")).toBe("#ff0000");
expect(theme2hex("ReD")).toBe("#ff0000");
expect(theme2hex("#ff0000")).toBe("#ff0000");
expect(theme2hex("unicorn")).toBe("unicorn");
expect(theme2hex("#abc")).toBe("#aabbcc");
@@ -1,7 +1,10 @@
import { assert, describe, it } from "vitest";
import type { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
import type { LovelaceSectionConfig } from "../../../../src/data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
import {
duplicateSection,
moveCardToContainer,
swapView,
} from "../../../../src/panels/lovelace/editor/config-util";
@@ -141,3 +144,84 @@ describe("swapView", () => {
assert.deepEqual(expected, result);
});
});
describe("duplicateSection", () => {
it("inserts a clone immediately after the original section", () => {
const config: LovelaceConfig = {
views: [
{
sections: [
{ type: "grid", cards: [{ type: "button" }] },
{ type: "grid", cards: [{ type: "heading" }] },
],
},
],
};
const result = duplicateSection(config, 0, 0);
const expected: LovelaceConfig = {
views: [
{
sections: [
{ type: "grid", cards: [{ type: "button" }] },
{ type: "grid", cards: [{ type: "button" }] },
{ type: "grid", cards: [{ type: "heading" }] },
],
},
],
};
assert.deepEqual(expected, result);
});
it("preserves all cards and properties within the cloned section", () => {
const config: LovelaceConfig = {
views: [
{
sections: [
{
type: "grid",
column_span: 2,
cards: [{ type: "button" }, { type: "heading" }],
},
],
},
],
};
const result = duplicateSection(config, 0, 0);
const view = result.views[0] as LovelaceViewConfig;
assert.equal(view.sections!.length, 2);
assert.deepEqual(view.sections![0], view.sections![1]);
});
it("produces a deep clone, changes do not affect the original", () => {
const config: LovelaceConfig = {
views: [
{
sections: [
{
type: "grid",
column_span: 2,
cards: [{ type: "button" }, { type: "heading" }],
},
],
},
],
};
const result = duplicateSection(config, 0, 0);
const resultSections = (result.views[0] as LovelaceViewConfig).sections!;
assert.equal(resultSections.length, 2);
assert.deepEqual(resultSections[0], resultSections[1]);
(resultSections[1] as LovelaceSectionConfig).cards![0].type = "heading";
assert.equal(
(resultSections[0] as LovelaceSectionConfig).cards![0].type,
"button"
);
});
});
+73 -73
View File
@@ -4170,12 +4170,12 @@ __metadata:
languageName: node
linkType: hard
"@swc/helpers@npm:0.5.19":
version: 0.5.19
resolution: "@swc/helpers@npm:0.5.19"
"@swc/helpers@npm:0.5.20":
version: 0.5.20
resolution: "@swc/helpers@npm:0.5.20"
dependencies:
tslib: "npm:^2.8.0"
checksum: 10/3fd365fb3265f97e1241bcbcea9bfa5e15e03c630424e1b54597e00d30be2c271cb0c74f45e1739c6bc5ae892647302fab412de5138941aa96e66aebf4586700
checksum: 10/a46030291484f8fd57505c4ae13cb179aa1f0cef201b14a065d857cfe3c3f41aab46d410a9cec7785f4768ac5b78dc4d07c344086c0ea2cacf67ba034fbed7a2
languageName: node
linkType: hard
@@ -5114,12 +5114,12 @@ __metadata:
languageName: node
linkType: hard
"@vitest/coverage-v8@npm:4.1.1":
version: 4.1.1
resolution: "@vitest/coverage-v8@npm:4.1.1"
"@vitest/coverage-v8@npm:4.1.2":
version: 4.1.2
resolution: "@vitest/coverage-v8@npm:4.1.2"
dependencies:
"@bcoe/v8-coverage": "npm:^1.0.2"
"@vitest/utils": "npm:4.1.1"
"@vitest/utils": "npm:4.1.2"
ast-v8-to-istanbul: "npm:^1.0.0"
istanbul-lib-coverage: "npm:^3.2.2"
istanbul-lib-report: "npm:^3.0.1"
@@ -5127,36 +5127,36 @@ __metadata:
magicast: "npm:^0.5.2"
obug: "npm:^2.1.1"
std-env: "npm:^4.0.0-rc.1"
tinyrainbow: "npm:^3.0.3"
tinyrainbow: "npm:^3.1.0"
peerDependencies:
"@vitest/browser": 4.1.1
vitest: 4.1.1
"@vitest/browser": 4.1.2
vitest: 4.1.2
peerDependenciesMeta:
"@vitest/browser":
optional: true
checksum: 10/e5873ac0a40fa34772a68f448910cc1d77c97f2c8d1701adcb1d33411d443aa652b851aacbfc99175c3de136fa038eb83f6321321e1dc371999f3dc96999dd69
checksum: 10/2a38252da937894dfd47a20839714cd49deb8ea0b8289fe25ba17b6677b99dc9b695e4c689b1d6532f19e0d1b81dbac2cf555f82a0ae75abf490dd4107407206
languageName: node
linkType: hard
"@vitest/expect@npm:4.1.1":
version: 4.1.1
resolution: "@vitest/expect@npm:4.1.1"
"@vitest/expect@npm:4.1.2":
version: 4.1.2
resolution: "@vitest/expect@npm:4.1.2"
dependencies:
"@standard-schema/spec": "npm:^1.1.0"
"@types/chai": "npm:^5.2.2"
"@vitest/spy": "npm:4.1.1"
"@vitest/utils": "npm:4.1.1"
"@vitest/spy": "npm:4.1.2"
"@vitest/utils": "npm:4.1.2"
chai: "npm:^6.2.2"
tinyrainbow: "npm:^3.0.3"
checksum: 10/eb74aee01c3c1be58aeb829be6b112600ff34703f2c247ad993db10375d9af87d03c294c485fa6f56754b8af130cc600b397eca081c29d1a2a36b8286e3d0fbd
tinyrainbow: "npm:^3.1.0"
checksum: 10/536c5a8903927e324bbb66967be4e0ec2ec4ff6234f0b8fe20987841b0705c931c7e3ce2e61c7665f4ded65ba736de6cda8d2d37ee114efeedb187ca5d597ea1
languageName: node
linkType: hard
"@vitest/mocker@npm:4.1.1":
version: 4.1.1
resolution: "@vitest/mocker@npm:4.1.1"
"@vitest/mocker@npm:4.1.2":
version: 4.1.2
resolution: "@vitest/mocker@npm:4.1.2"
dependencies:
"@vitest/spy": "npm:4.1.1"
"@vitest/spy": "npm:4.1.2"
estree-walker: "npm:^3.0.3"
magic-string: "npm:^0.30.21"
peerDependencies:
@@ -5167,56 +5167,56 @@ __metadata:
optional: true
vite:
optional: true
checksum: 10/76a50f0af8d5e8ffaa29a944be6ac7acade9fd48d9ddd766a0a27ccbeeb80e256845be7f99c383fe235a9edf66d6801ae698505b577060068f181ec25f3e156a
checksum: 10/1d7976e19ef168357aba2ca41cd8db86236a98dfb2209bd3152a3a20e9a5b8cbfd8f73356c43a934b384d3b4c7a63835fb1037d3f56a7824faa838331eaa214e
languageName: node
linkType: hard
"@vitest/pretty-format@npm:4.1.1":
version: 4.1.1
resolution: "@vitest/pretty-format@npm:4.1.1"
"@vitest/pretty-format@npm:4.1.2":
version: 4.1.2
resolution: "@vitest/pretty-format@npm:4.1.2"
dependencies:
tinyrainbow: "npm:^3.0.3"
checksum: 10/89c260f8361ce11345677ff5f1b99549ad4bde9a38b329885cb20815461b41c7ea6425e4822a7ecdf4ead536cc0dc8757f3a8387f5e9982c741cbb3c8b3eb0cb
tinyrainbow: "npm:^3.1.0"
checksum: 10/a07a6023c52b25be5c75fc05bb3317629390cc1b50eae6cbea91ba4c13193ec88e54abaa56b46b40ddb8a6a4558d667f2ba0e1cf2ee2d0e32b463244f3002aa7
languageName: node
linkType: hard
"@vitest/runner@npm:4.1.1":
version: 4.1.1
resolution: "@vitest/runner@npm:4.1.1"
"@vitest/runner@npm:4.1.2":
version: 4.1.2
resolution: "@vitest/runner@npm:4.1.2"
dependencies:
"@vitest/utils": "npm:4.1.1"
"@vitest/utils": "npm:4.1.2"
pathe: "npm:^2.0.3"
checksum: 10/65a4374a4385d2ffccf98110ba7a7521cf9e90ec68b93901f1d5b07f5b574a17fb26e8631d548f703ddaf4ded09b91dc05e4415a986d6e87556689385337c5e7
checksum: 10/13fd019a63ee3225420474cbd1ca0ae7c5c2dcdd241f2a958ca45731c10de36131f15303ae8ab1196133ec4e955b7c6de658c7b5e19736d550f310c8195fa9b2
languageName: node
linkType: hard
"@vitest/snapshot@npm:4.1.1":
version: 4.1.1
resolution: "@vitest/snapshot@npm:4.1.1"
"@vitest/snapshot@npm:4.1.2":
version: 4.1.2
resolution: "@vitest/snapshot@npm:4.1.2"
dependencies:
"@vitest/pretty-format": "npm:4.1.1"
"@vitest/utils": "npm:4.1.1"
"@vitest/pretty-format": "npm:4.1.2"
"@vitest/utils": "npm:4.1.2"
magic-string: "npm:^0.30.21"
pathe: "npm:^2.0.3"
checksum: 10/2f347abfeedb4c2aacad8b0db472842383c5369ed972b65634f327d31c91adc96055426b0dbedcc3275392a57561cffc28d76c526044368fabd1f9ca28e25572
checksum: 10/9d124412dbe44db43ca5277180bf5fe5dad7373218a177830bba631b53d225f7d4de368a20d6f5740ec07402e9e4dd179609db2b2f691d2d8b02f1bdbfd8c1a3
languageName: node
linkType: hard
"@vitest/spy@npm:4.1.1":
version: 4.1.1
resolution: "@vitest/spy@npm:4.1.1"
checksum: 10/50dbb99bc4f49b8779dbbe258c3665aa123720560942ec0db18e983abdea9094c54ce137f627eeddc66460f9c6631df273dbf56109d813eca74a85c2188a4548
"@vitest/spy@npm:4.1.2":
version: 4.1.2
resolution: "@vitest/spy@npm:4.1.2"
checksum: 10/e20e417ac430fee34e4be58802b2eb31e1c1163296a8921c0878be14e1ae77c7a7cae1b9b515d56fe623e05ee21b092aff7eb5e0d412f656650b72ecd02bb30a
languageName: node
linkType: hard
"@vitest/utils@npm:4.1.1":
version: 4.1.1
resolution: "@vitest/utils@npm:4.1.1"
"@vitest/utils@npm:4.1.2":
version: 4.1.2
resolution: "@vitest/utils@npm:4.1.2"
dependencies:
"@vitest/pretty-format": "npm:4.1.1"
"@vitest/pretty-format": "npm:4.1.2"
convert-source-map: "npm:^2.0.0"
tinyrainbow: "npm:^3.0.3"
checksum: 10/11757c339d2942c43ea8bc105a02fda82696cfd78ad70078d4f0784a8699ff8fb811415a73b955291c71fe2b50a822951b42ebcbb2ccc5180fc5757b2eb598a8
tinyrainbow: "npm:^3.1.0"
checksum: 10/854decf0eb639758d012c9aa53c3d7aed547e37c05ece6704d5f53035be77f704a24973ed95089926e1768c0b55902d42c4438660788e7a0f0e80d0fda1c713b
languageName: node
linkType: hard
@@ -8900,7 +8900,7 @@ __metadata:
"@rsdoctor/rspack-plugin": "npm:1.5.5"
"@rspack/core": "npm:1.7.10"
"@rspack/dev-server": "npm:1.2.1"
"@swc/helpers": "npm:0.5.19"
"@swc/helpers": "npm:0.5.20"
"@thomasloven/round-slider": "npm:0.6.0"
"@tsparticles/engine": "npm:3.9.1"
"@tsparticles/preset-links": "npm:3.2.0"
@@ -8922,7 +8922,7 @@ __metadata:
"@types/tar": "npm:7.0.87"
"@types/webspeechapi": "npm:0.0.29"
"@vibrant/color": "npm:4.0.4"
"@vitest/coverage-v8": "npm:4.1.1"
"@vitest/coverage-v8": "npm:4.1.2"
"@webcomponents/scoped-custom-element-registry": "npm:0.0.10"
"@webcomponents/webcomponentsjs": "npm:2.8.0"
babel-loader: "npm:10.1.1"
@@ -9005,7 +9005,7 @@ __metadata:
typescript: "npm:5.9.3"
typescript-eslint: "npm:8.57.2"
vite-tsconfig-paths: "npm:6.1.1"
vitest: "npm:4.1.1"
vitest: "npm:4.1.2"
webpack-stats-plugin: "npm:1.1.3"
webpackbar: "npm:7.0.0"
weekstart: "npm:2.0.0"
@@ -13608,10 +13608,10 @@ __metadata:
languageName: node
linkType: hard
"tinyrainbow@npm:^3.0.3":
version: 3.0.3
resolution: "tinyrainbow@npm:3.0.3"
checksum: 10/169cc63c15e1378674180f3207c82c05bfa58fc79992e48792e8d97b4b759012f48e95297900ede24a81f0087cf329a0d85bb81109739eacf03c650127b3f6c1
"tinyrainbow@npm:^3.1.0":
version: 3.1.0
resolution: "tinyrainbow@npm:3.1.0"
checksum: 10/4c2c01dde1e5bb9a74973daaae141d4d733d246280b2f9a7f6a9e7dd8e940d48b2580a6086125278777897bc44635d6ccec5f9f563c2179dd2129f4542d0ec05
languageName: node
linkType: hard
@@ -14321,17 +14321,17 @@ __metadata:
languageName: node
linkType: hard
"vitest@npm:4.1.1":
version: 4.1.1
resolution: "vitest@npm:4.1.1"
"vitest@npm:4.1.2":
version: 4.1.2
resolution: "vitest@npm:4.1.2"
dependencies:
"@vitest/expect": "npm:4.1.1"
"@vitest/mocker": "npm:4.1.1"
"@vitest/pretty-format": "npm:4.1.1"
"@vitest/runner": "npm:4.1.1"
"@vitest/snapshot": "npm:4.1.1"
"@vitest/spy": "npm:4.1.1"
"@vitest/utils": "npm:4.1.1"
"@vitest/expect": "npm:4.1.2"
"@vitest/mocker": "npm:4.1.2"
"@vitest/pretty-format": "npm:4.1.2"
"@vitest/runner": "npm:4.1.2"
"@vitest/snapshot": "npm:4.1.2"
"@vitest/spy": "npm:4.1.2"
"@vitest/utils": "npm:4.1.2"
es-module-lexer: "npm:^2.0.0"
expect-type: "npm:^1.3.0"
magic-string: "npm:^0.30.21"
@@ -14342,17 +14342,17 @@ __metadata:
tinybench: "npm:^2.9.0"
tinyexec: "npm:^1.0.2"
tinyglobby: "npm:^0.2.15"
tinyrainbow: "npm:^3.0.3"
tinyrainbow: "npm:^3.1.0"
vite: "npm:^6.0.0 || ^7.0.0 || ^8.0.0"
why-is-node-running: "npm:^2.3.0"
peerDependencies:
"@edge-runtime/vm": "*"
"@opentelemetry/api": ^1.9.0
"@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0
"@vitest/browser-playwright": 4.1.1
"@vitest/browser-preview": 4.1.1
"@vitest/browser-webdriverio": 4.1.1
"@vitest/ui": 4.1.1
"@vitest/browser-playwright": 4.1.2
"@vitest/browser-preview": 4.1.2
"@vitest/browser-webdriverio": 4.1.2
"@vitest/ui": 4.1.2
happy-dom: "*"
jsdom: "*"
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -14379,7 +14379,7 @@ __metadata:
optional: false
bin:
vitest: vitest.mjs
checksum: 10/2dc81153729c57e6b3ad0aaa920a4f026b717df12741c4c520b8cd49b92fc6e21e9af1ac7568249383521311fcde28f1dfd79a666fd679316016958838249b1b
checksum: 10/6b037387e59d403f6570f887f6ac96b81ff6e768dbd02d32a812ddff5bdebef022dd6d9f20b84fb9535866e0c5dbdf80e6705cc428b6a8f8a8e67e1335235848
languageName: node
linkType: hard