mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-02 08:54:03 +00:00
Compare commits
1 Commits
retro-east
...
remove-app
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ecb41accd |
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -41,14 +41,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.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@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.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@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
|
||||
@@ -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.20",
|
||||
"@swc/helpers": "0.5.19",
|
||||
"@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.2",
|
||||
"@vitest/coverage-v8": "4.1.1",
|
||||
"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.2",
|
||||
"vitest": "4.1.1",
|
||||
"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"
|
||||
|
||||
@@ -32,12 +32,6 @@ 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`;
|
||||
@@ -45,12 +39,6 @@ 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) {
|
||||
@@ -59,22 +47,6 @@ 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.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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);
|
||||
@@ -131,43 +130,26 @@ export const rgb2hs = (rgb: [number, number, number]): [number, number] =>
|
||||
export const hs2rgb = (hs: [number, number]): [number, number, number] =>
|
||||
hsv2rgb([hs[0], hs[1], 255]);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
export function theme2hex(themeColor: string): string {
|
||||
if (themeColor.startsWith("#")) {
|
||||
if (themeColor.length === 4 || themeColor.length === 5) {
|
||||
const c = themeColor;
|
||||
// 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 (color.length === 9) {
|
||||
if (themeColor.length === 9) {
|
||||
// Ignore alpha channel.
|
||||
return color.substring(0, 7);
|
||||
return themeColor.substring(0, 7);
|
||||
}
|
||||
return color;
|
||||
return themeColor;
|
||||
}
|
||||
|
||||
// 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 rgbFromColorName = colors[themeColor.toLowerCase()];
|
||||
if (rgbFromColorName) {
|
||||
return rgb2hex(rgbFromColorName);
|
||||
}
|
||||
|
||||
// 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+)/);
|
||||
const rgbMatch = themeColor.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
||||
if (rgbMatch) {
|
||||
const [, r, g, b] = rgbMatch.map(Number);
|
||||
return rgb2hex([r, g, b]);
|
||||
@@ -176,5 +158,5 @@ export function theme2hex(color: 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 color;
|
||||
return themeColor;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { wcagLuminance, wcagContrast } from "culori";
|
||||
import { theme2hex } from "./convert-color";
|
||||
|
||||
/**
|
||||
* Calculates the luminosity of an RGB color.
|
||||
@@ -49,13 +48,3 @@ 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";
|
||||
};
|
||||
|
||||
@@ -5,41 +5,12 @@ 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,
|
||||
|
||||
@@ -91,10 +91,6 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _lastTapTime?: number;
|
||||
|
||||
private _longPressTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _longPressTriggered = false;
|
||||
|
||||
private _shouldResizeChart = false;
|
||||
|
||||
private _resizeAnimationDuration?: number;
|
||||
@@ -132,7 +128,6 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._legendPointerCancel();
|
||||
this._pendingSetup = false;
|
||||
while (this._listeners.length) {
|
||||
this._listeners.pop()!();
|
||||
@@ -307,31 +302,22 @@ export class HaChartBase extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _getLegendItems() {
|
||||
private _renderLegend() {
|
||||
if (!this.options?.legend || !this.data) {
|
||||
return undefined;
|
||||
return nothing;
|
||||
}
|
||||
const legend = ensureArray(this.options.legend).find(
|
||||
(l) => l.show && l.type === "custom"
|
||||
) as CustomLegendOption | undefined;
|
||||
if (!legend) {
|
||||
return undefined;
|
||||
return nothing;
|
||||
}
|
||||
const datasets = ensureArray(this.data);
|
||||
return (
|
||||
const items =
|
||||
legend.data ||
|
||||
datasets
|
||||
.filter((d) => (d.data as any[])?.length && (d.id || 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!);
|
||||
.map((d) => ({ id: d.id, name: d.name }));
|
||||
|
||||
const isMobile = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
@@ -376,11 +362,6 @@ 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}
|
||||
>
|
||||
@@ -651,7 +632,7 @@ export class HaChartBase extends LitElement {
|
||||
hideOverlap: true,
|
||||
...axis.axisLabel,
|
||||
},
|
||||
minInterval: axis.minInterval ?? minInterval,
|
||||
minInterval,
|
||||
} as XAXisOption;
|
||||
});
|
||||
}
|
||||
@@ -1041,52 +1022,11 @@ export class HaChartBase extends LitElement {
|
||||
fireEvent(this, "chart-zoom", { start, end });
|
||||
}
|
||||
|
||||
// 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) {
|
||||
private _legendClick(ev: any) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
const id = ev.currentTarget?.id;
|
||||
if (this._hiddenDatasets.has(id)) {
|
||||
this._getAllIdsFromLegend(this.options, id).forEach((i) =>
|
||||
this._hiddenDatasets.delete(i)
|
||||
@@ -1101,60 +1041,6 @@ 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(() => {
|
||||
|
||||
@@ -65,8 +65,6 @@ 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");
|
||||
|
||||
@@ -96,7 +94,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _reducedMotion = false;
|
||||
|
||||
@state() private _physicsEnabled?: boolean;
|
||||
@state() private _physicsEnabled = true;
|
||||
|
||||
@state() private _showLabels = true;
|
||||
|
||||
@@ -124,14 +122,6 @@ 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;
|
||||
@@ -148,7 +138,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.data=${this._getSeries(
|
||||
this.data,
|
||||
this._physicsEnabled ?? false,
|
||||
this._physicsEnabled,
|
||||
this._reducedMotion,
|
||||
this._showLabels,
|
||||
isMobile,
|
||||
|
||||
@@ -32,7 +32,6 @@ 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";
|
||||
|
||||
@@ -294,22 +293,6 @@ 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,6 +2,7 @@ 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";
|
||||
@@ -52,15 +53,16 @@ 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
|
||||
@@ -100,6 +102,10 @@ 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);
|
||||
|
||||
@@ -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, queryAll, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
|
||||
import {
|
||||
formatCallyDateRange,
|
||||
@@ -29,7 +29,6 @@ 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 {
|
||||
@@ -70,8 +69,6 @@ export class DateRangePicker extends LitElement {
|
||||
to: { hours: 23, minutes: 59 },
|
||||
};
|
||||
|
||||
@queryAll("ha-time-input") private _timeInputs?: NodeListOf<HaTimeInput>;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -156,7 +153,6 @@ 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}`}
|
||||
@@ -167,7 +163,6 @@ export class DateRangePicker extends LitElement {
|
||||
)}
|
||||
id="to"
|
||||
placeholder-labels
|
||||
auto-validate
|
||||
></ha-time-input>
|
||||
</div>
|
||||
`
|
||||
@@ -205,14 +200,6 @@ 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);
|
||||
@@ -274,6 +261,12 @@ 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],
|
||||
@@ -288,8 +281,7 @@ export class DateRangePicker extends LitElement {
|
||||
private _handleChangeTime(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const time = ev.detail.value;
|
||||
const target = ev.target as HaBaseTimeInput;
|
||||
const type = target.id;
|
||||
const type = (ev.target as HaBaseTimeInput).id;
|
||||
if (time) {
|
||||
if (!this._timeValue) {
|
||||
this._timeValue = {
|
||||
@@ -309,39 +301,17 @@ 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 {
|
||||
@@ -356,6 +326,12 @@ 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%;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -80,6 +80,33 @@ 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`
|
||||
|
||||
@@ -137,7 +137,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
@property({ attribute: "placeholder-labels", type: Boolean })
|
||||
public placeholderLabels = false;
|
||||
|
||||
@queryAll("ha-input") private _inputs?: NodeListOf<HaInput>;
|
||||
@queryAll("ha-input") private _inputs?: HaInput[];
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
@@ -145,9 +145,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const inputs = this._inputs;
|
||||
if (!inputs) return true;
|
||||
return [...inputs].every((input) => input.reportValidity());
|
||||
return this._inputs?.every((input) => input.reportValidity()) ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -401,7 +399,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
|
||||
.time-separator,
|
||||
ha-icon-button {
|
||||
background-color: var(--ha-color-form-background);
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
color: var(--ha-color-text-secondary);
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-loud);
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -100,9 +100,6 @@ export class HaDropdown extends Dropdown {
|
||||
#menu {
|
||||
padding: var(--ha-space-1);
|
||||
}
|
||||
wa-popup::part(popup) {
|
||||
z-index: 200;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ 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";
|
||||
@@ -97,14 +98,17 @@ export class HaFilterLabels extends LitElement {
|
||||
this.value
|
||||
),
|
||||
(label) => label.label_id,
|
||||
(label) =>
|
||||
html`<ha-check-list-item
|
||||
(label) => {
|
||||
const color = label.color
|
||||
? computeCssColor(label.color)
|
||||
: undefined;
|
||||
return html`<ha-check-list-item
|
||||
.value=${label.label_id}
|
||||
.selected=${(this.value || []).includes(label.label_id)}
|
||||
hasMeta
|
||||
>
|
||||
<ha-label
|
||||
.color=${label.color}
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
@@ -115,7 +119,8 @@ export class HaFilterLabels extends LitElement {
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-check-list-item>`
|
||||
</ha-check-list-item>`;
|
||||
}
|
||||
)}
|
||||
</ha-list> `
|
||||
: nothing}
|
||||
@@ -248,6 +253,10 @@ 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;
|
||||
|
||||
@@ -1,44 +1,18 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, 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
|
||||
@@ -62,6 +36,10 @@ 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;
|
||||
|
||||
@@ -6,6 +6,7 @@ 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";
|
||||
@@ -16,7 +17,6 @@ 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,14 +106,9 @@ 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 {
|
||||
@@ -140,6 +135,9 @@ 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
|
||||
@@ -156,7 +154,7 @@ export class HaLabelsPicker extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.label=${label.name}
|
||||
selected
|
||||
style=${label.style}
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon
|
||||
@@ -241,10 +239,8 @@ export class HaLabelsPicker extends LitElement {
|
||||
height: var(--ha-space-8);
|
||||
}
|
||||
ha-input-chip {
|
||||
--md-input-chip-selected-container-color: var(
|
||||
--ha-label-background-color,
|
||||
var(--grey-color)
|
||||
);
|
||||
--md-input-chip-selected-container-color: var(--color, var(--grey-color));
|
||||
--ha-input-chip-selected-container-opacity: 0.5;
|
||||
--md-input-chip-selected-outline-width: 1px;
|
||||
}
|
||||
label {
|
||||
|
||||
@@ -153,7 +153,10 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
right: 0;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: var(--ha-color-border-neutral-loud);
|
||||
background-color: var(
|
||||
--mdc-text-field-idle-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
transform:
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
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)",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,683 +0,0 @@
|
||||
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,9 +287,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
const numberSelector = {
|
||||
number: {
|
||||
...this.selector.numeric_threshold?.number,
|
||||
...(!showUnit && effectiveUnit
|
||||
? { unit_of_measurement: effectiveUnit }
|
||||
: {}),
|
||||
...(effectiveUnit ? { unit_of_measurement: effectiveUnit } : {}),
|
||||
},
|
||||
};
|
||||
const entitySelector = {
|
||||
|
||||
@@ -21,8 +21,6 @@ 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;
|
||||
|
||||
@@ -73,7 +71,6 @@ 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"
|
||||
@@ -89,7 +86,6 @@ 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.
|
||||
@@ -101,8 +97,6 @@ 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;
|
||||
}
|
||||
@@ -121,17 +115,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
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";
|
||||
@@ -31,12 +25,6 @@ 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;
|
||||
@@ -175,10 +163,6 @@ export class HaToast extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const hasAction =
|
||||
(this._actionElements?.length ?? 0) > 0 ||
|
||||
(this._dismissElements?.length ?? 0) > 0;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -191,7 +175,7 @@ export class HaToast extends LitElement {
|
||||
popover=${ifDefined(popoverSupported ? "manual" : undefined)}
|
||||
>
|
||||
<span class="message">${this.labelText}</span>
|
||||
<div class=${classMap({ actions: true, "has-action": hasAction })}>
|
||||
<div class="actions">
|
||||
<slot name="action"></slot>
|
||||
<slot name="dismiss"></slot>
|
||||
</div>
|
||||
@@ -214,13 +198,21 @@ export class HaToast extends LitElement {
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
min-width: min(350px, calc(var(--safe-width) - var(--ha-space-4)));
|
||||
max-width: min(650px, var(--safe-width));
|
||||
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-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-3) var(--ha-space-4);
|
||||
padding: var(--ha-space-3);
|
||||
color: var(--ha-color-on-neutral-loud);
|
||||
background-color: var(--ha-color-neutral-10);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
@@ -253,14 +245,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: var(--safe-width);
|
||||
max-width: var(--safe-width);
|
||||
min-width: calc(
|
||||
100vw - var(--safe-area-inset-left, 0px) - var(
|
||||
--safe-area-inset-right,
|
||||
0px
|
||||
)
|
||||
);
|
||||
border-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
|
||||
export type ThemeVars = Record<string, string>;
|
||||
export interface ThemeVars {
|
||||
// Incomplete
|
||||
"primary-color": string;
|
||||
"text-primary-color": string;
|
||||
"accent-color": string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export type Theme = ThemeVars & {
|
||||
modes?: {
|
||||
|
||||
@@ -82,7 +82,6 @@ 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);
|
||||
|
||||
@@ -52,7 +52,6 @@ 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}
|
||||
@@ -80,7 +79,6 @@ 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 =
|
||||
|
||||
@@ -486,17 +486,6 @@ class SupervisorAppInfo extends LitElement {
|
||||
|
||||
<div class="description light-color">
|
||||
${this.addon.description}.<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.visit_app_page",
|
||||
{
|
||||
name: html`<a
|
||||
href=${this.addon.url!}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${getAppDisplayName(this.addon.name, this.addon.stage)}</a
|
||||
>`,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div class="addon-container">
|
||||
<div>
|
||||
@@ -1038,7 +1027,6 @@ class SupervisorAppInfo extends LitElement {
|
||||
}
|
||||
|
||||
private _updateComplete() {
|
||||
this._scheduleDataUpdate();
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
@@ -1060,16 +1048,11 @@ class SupervisorAppInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
showConfirmationDialog(this, {
|
||||
showAlertDialog(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,6 +25,7 @@ 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";
|
||||
@@ -1321,6 +1322,7 @@ ${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)
|
||||
);
|
||||
@@ -1340,7 +1342,10 @@ ${rejected
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label .color=${label.color} .description=${label.description}>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
@@ -1485,6 +1490,10 @@ ${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,6 +3,7 @@ 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";
|
||||
@@ -171,9 +172,13 @@ 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
|
||||
.color=${label?.color}
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label?.description}
|
||||
>
|
||||
${label?.icon
|
||||
@@ -238,6 +243,12 @@ 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,9 +10,11 @@ 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";
|
||||
@@ -34,6 +36,7 @@ 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";
|
||||
@@ -678,6 +681,7 @@ 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)
|
||||
);
|
||||
@@ -699,7 +703,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label
|
||||
.color=${label.color}
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description || undefined}
|
||||
>
|
||||
${label.icon
|
||||
@@ -1250,6 +1254,10 @@ ${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,6 +23,7 @@ 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";
|
||||
@@ -753,6 +754,7 @@ 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)
|
||||
);
|
||||
@@ -772,7 +774,10 @@ export class HaConfigEntities extends LitElement {
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label .color=${label.color} .description=${label.description}>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
@@ -1673,6 +1678,10 @@ ${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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ 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";
|
||||
@@ -1428,6 +1429,7 @@ ${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)
|
||||
);
|
||||
@@ -1447,7 +1449,10 @@ ${rejected
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label .color=${label.color} .description=${label.description}>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
@@ -1533,6 +1538,10 @@ ${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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -46,14 +46,11 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
const featuresToSort = [...features];
|
||||
|
||||
return featuresToSort.sort((a, b) => {
|
||||
// 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;
|
||||
// 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;
|
||||
|
||||
// Sort everything else alphabetically
|
||||
return domainToName(localize, a.domain).localeCompare(
|
||||
|
||||
@@ -22,6 +22,7 @@ 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";
|
||||
@@ -1115,6 +1116,7 @@ ${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)
|
||||
);
|
||||
@@ -1134,7 +1136,10 @@ ${rejected
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label .color=${label.color} .description=${label.description}>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
@@ -1267,6 +1272,10 @@ ${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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ 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";
|
||||
@@ -1174,6 +1175,7 @@ ${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)
|
||||
);
|
||||
@@ -1193,7 +1195,10 @@ ${rejected
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label .color=${label.color} .description=${label.description}>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
@@ -1325,6 +1330,10 @@ ${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,7 +35,6 @@ 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
|
||||
@@ -110,7 +109,17 @@ export function getCommonOptions(
|
||||
type: "time",
|
||||
min: subDays(start, MONTH_TIME_AXIS_PADDING),
|
||||
max: addDays(suggestedMax, MONTH_TIME_AXIS_PADDING),
|
||||
axisLabel: getPeriodicAxisLabelConfig("month", locale, config),
|
||||
axisLabel: {
|
||||
formatter: {
|
||||
year: "{yearStyle|{MMMM} {yyyy}}",
|
||||
month: "{MMMM}",
|
||||
},
|
||||
rich: {
|
||||
yearStyle: {
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
// 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,7 +43,6 @@ const COLORS: Record<HomeSummary, string> = {
|
||||
security: "blue-grey",
|
||||
media_players: "blue",
|
||||
energy: "amber",
|
||||
persons: "green",
|
||||
};
|
||||
|
||||
@customElement("hui-home-summary-card")
|
||||
@@ -258,21 +257,6 @@ 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 "";
|
||||
}
|
||||
|
||||
@@ -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 { resolveThemeColor } from "../../../common/color/compute-color";
|
||||
import { computeCssVariableName } 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,6 +431,15 @@ 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 [];
|
||||
@@ -470,7 +479,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
...(this._configEntities || []).map((entityConf) => ({
|
||||
entity_id: entityConf.entity,
|
||||
color: entityConf.color
|
||||
? resolveThemeColor(entityConf.color)
|
||||
? this._resolveColor(entityConf.color)
|
||||
: this._getColor(entityConf.entity),
|
||||
label_mode: entityConf.label_mode,
|
||||
attribute: entityConf.attribute,
|
||||
@@ -532,7 +541,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
name,
|
||||
fullDatetime: (config.hours_to_show ?? DEFAULT_HOURS_TO_SHOW) > 144,
|
||||
color: entityConfig?.color
|
||||
? resolveThemeColor(entityConfig.color)
|
||||
? this._resolveColor(entityConfig.color)
|
||||
: this._getColor(entityId),
|
||||
gradualOpacity: 0.8,
|
||||
});
|
||||
|
||||
@@ -19,10 +19,8 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import {
|
||||
ensureBadgeConfig,
|
||||
type LovelaceBadgeConfig,
|
||||
} from "../../../data/lovelace/config/badge";
|
||||
import { ensureBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
|
||||
@@ -60,7 +58,7 @@ export class HuiBadgeEditMode extends LitElement {
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
protected _clipboard?: string | Partial<LovelaceBadgeConfig>;
|
||||
protected _clipboard?: LovelaceCardConfig;
|
||||
|
||||
private get _badges() {
|
||||
const containerPath = getLovelaceContainerPath(this.path!);
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiDragHorizontalVariant,
|
||||
mdiPencil,
|
||||
mdiPlusCircleMultipleOutline,
|
||||
} from "@mdi/js";
|
||||
import { mdiDelete, mdiDragHorizontalVariant, mdiPencil } 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, duplicateSection } from "../editor/config-util";
|
||||
import { deleteSection } 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";
|
||||
@@ -41,32 +31,16 @@ export class HuiSectionEditMode extends LitElement {
|
||||
class="handle"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-wrapper">
|
||||
@@ -75,23 +49,8 @@ export class HuiSectionEditMode extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
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() {
|
||||
private async _editSection(ev) {
|
||||
ev.stopPropagation();
|
||||
showEditSectionDialog(this, {
|
||||
lovelace: this.lovelace!,
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
@@ -103,16 +62,8 @@ export class HuiSectionEditMode extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _duplicateSection(): void {
|
||||
const newConfig = duplicateSection(
|
||||
this.lovelace!.config,
|
||||
this.viewIndex,
|
||||
this.index
|
||||
);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
}
|
||||
|
||||
private async _deleteSection() {
|
||||
private async _deleteSection(ev) {
|
||||
ev.stopPropagation();
|
||||
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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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";
|
||||
@@ -304,19 +303,6 @@ 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-s));
|
||||
font-size: var(--ha-heading-badge-font-size, var(--ha-font-size-m));
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
ha-control-button.with-text {
|
||||
@@ -135,9 +135,6 @@ export class HuiButtonHeadingBadge
|
||||
padding: 0 var(--ha-space-1);
|
||||
line-height: 1;
|
||||
}
|
||||
ha-icon {
|
||||
line-height: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -110,19 +110,11 @@ 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) {
|
||||
@@ -152,11 +144,7 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
if (changedProperties.has("_cards")) {
|
||||
this._layoutElement.cards = this._cards;
|
||||
}
|
||||
if (
|
||||
changedProperties.has("hass") ||
|
||||
changedProperties.has("preview") ||
|
||||
changedProperties.has("_cards")
|
||||
) {
|
||||
if (changedProperties.has("hass") || changedProperties.has("preview")) {
|
||||
this._updateVisibility();
|
||||
}
|
||||
}
|
||||
@@ -212,10 +200,6 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
}
|
||||
}
|
||||
|
||||
private _cardVisibilityChanged = () => {
|
||||
this._updateVisibility();
|
||||
};
|
||||
|
||||
protected _updateVisibility(conditionsMet?: boolean) {
|
||||
if (!this._layoutElement || !this._config) {
|
||||
return;
|
||||
@@ -236,16 +220,7 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
(!this._config.visibility ||
|
||||
checkConditionsMet(this._config.visibility, this.hass));
|
||||
|
||||
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);
|
||||
this._setElementVisibility(visible);
|
||||
}
|
||||
|
||||
private _setElementVisibility(visible: boolean) {
|
||||
@@ -257,9 +232,9 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
fireEvent(this, "section-visibility-changed", { value: visible });
|
||||
}
|
||||
|
||||
// Always keep layout element connected so cards can still update
|
||||
// their visibility and bubble events back to the section.
|
||||
if (!this._layoutElement.parentElement) {
|
||||
if (!visible && this._layoutElement.parentElement) {
|
||||
this.removeChild(this._layoutElement);
|
||||
} else if (visible && !this._layoutElement.parentElement) {
|
||||
this.appendChild(this._layoutElement);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ export const HOME_SUMMARIES = [
|
||||
"security",
|
||||
"media_players",
|
||||
"energy",
|
||||
"persons",
|
||||
] as const;
|
||||
|
||||
export type HomeSummary = (typeof HOME_SUMMARIES)[number];
|
||||
@@ -21,7 +20,6 @@ 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[]> = {
|
||||
@@ -30,7 +28,6 @@ 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 = (
|
||||
|
||||
@@ -216,9 +216,7 @@
|
||||
"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_persons_home": "{count} {count, plural,\n one {person}\n other {people}\n}",
|
||||
"nobody_home": "No one home"
|
||||
"count_media_playing": "{count} {count, plural,\n one {playing}\n other {playing}\n}"
|
||||
},
|
||||
"toggle-group": {
|
||||
"all_off": "All off",
|
||||
@@ -2741,7 +2739,6 @@
|
||||
"current_version": "Current version: {version}",
|
||||
"changelog": "Changelog",
|
||||
"hostname": "Hostname",
|
||||
"visit_app_page": "Visit {name} page for more details.",
|
||||
"start": "Start",
|
||||
"stop": "Stop",
|
||||
"restart": "Restart",
|
||||
@@ -2868,8 +2865,7 @@
|
||||
"get_changelog": "Failed to get changelog",
|
||||
"start_invalid_config": "Invalid configuration",
|
||||
"go_to_config": "Go to configuration",
|
||||
"validate_config": "Failed to validate app configuration",
|
||||
"view_supervisor_logs": "View supervisor logs"
|
||||
"validate_config": "Failed to validate app configuration"
|
||||
},
|
||||
"uninstall_dialog": {
|
||||
"title": "Uninstall {name}?",
|
||||
@@ -5008,7 +5004,7 @@
|
||||
"before": "Before",
|
||||
"after": "After",
|
||||
"description": {
|
||||
"picker": "Triggers when a calendar event starts or ends.",
|
||||
"picker": "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}"
|
||||
}
|
||||
},
|
||||
@@ -5016,7 +5012,7 @@
|
||||
"label": "Device",
|
||||
"trigger": "Trigger",
|
||||
"description": {
|
||||
"picker": "Triggers when something happens to a device. Great way to start."
|
||||
"picker": "When something happens to a device. Great way to start."
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
@@ -5027,7 +5023,7 @@
|
||||
"context_user_picked": "User firing event",
|
||||
"context_user_pick": "Select user",
|
||||
"description": {
|
||||
"picker": "Triggers when an event is being received (event is an advanced concept in Home Assistant).",
|
||||
"picker": "When an event is being received (event is an advanced concept in Home Assistant).",
|
||||
"full": "When {eventTypes} event is fired"
|
||||
}
|
||||
},
|
||||
@@ -5039,7 +5035,7 @@
|
||||
"enter": "Enter",
|
||||
"leave": "Leave",
|
||||
"description": {
|
||||
"picker": "Triggers when an entity created by a geolocation platform appears in or disappears from a zone.",
|
||||
"picker": "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}"
|
||||
}
|
||||
},
|
||||
@@ -5051,7 +5047,7 @@
|
||||
"to": "To (optional)",
|
||||
"any_state_ignore_attributes": "Any state (ignoring attribute changes)",
|
||||
"description": {
|
||||
"picker": "Triggers when the state of an entity (or attribute) changes.",
|
||||
"picker": "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}"
|
||||
}
|
||||
},
|
||||
@@ -5061,7 +5057,7 @@
|
||||
"start": "Start",
|
||||
"shutdown": "Shutdown",
|
||||
"description": {
|
||||
"picker": "Triggers when Home Assistant starts up or shuts down.",
|
||||
"picker": "When Home Assistant starts up or shuts down.",
|
||||
"started": "When Home Assistant is started",
|
||||
"shutdown": "When Home Assistant is shut down"
|
||||
}
|
||||
@@ -5071,7 +5067,7 @@
|
||||
"topic": "Topic",
|
||||
"payload": "Payload (optional)",
|
||||
"description": {
|
||||
"picker": "Triggers when a specific message is received on a given MQTT topic.",
|
||||
"picker": "When a specific message is received on a given MQTT topic.",
|
||||
"full": "When an MQTT message has been received"
|
||||
}
|
||||
},
|
||||
@@ -5085,7 +5081,7 @@
|
||||
"type_value": "Fixed number",
|
||||
"type_input": "Numeric value of another entity",
|
||||
"description": {
|
||||
"picker": "Triggers when the numeric value of an entity''s state (or attribute''s value) crosses a given threshold.",
|
||||
"picker": "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 }"
|
||||
@@ -5102,7 +5098,7 @@
|
||||
"updated": "updated"
|
||||
},
|
||||
"description": {
|
||||
"picker": "Triggers when a persistent notification is added or removed.",
|
||||
"picker": "When a persistent notification is added or removed.",
|
||||
"full": "When a persistent notification is updated"
|
||||
}
|
||||
},
|
||||
@@ -5113,7 +5109,7 @@
|
||||
"sunset": "Sunset",
|
||||
"offset": "Offset in seconds or HH:MM:SS (optional)",
|
||||
"description": {
|
||||
"picker": "Triggers when the sun sets or rises.",
|
||||
"picker": "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 }"
|
||||
}
|
||||
@@ -5125,7 +5121,7 @@
|
||||
"delete": "Delete sentence",
|
||||
"confirm_delete": "Are you sure you want to delete this sentence?",
|
||||
"description": {
|
||||
"picker": "Triggers when Assist matches a sentence from a voice assistant.",
|
||||
"picker": "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"
|
||||
@@ -5134,7 +5130,7 @@
|
||||
"tag": {
|
||||
"label": "Tag",
|
||||
"description": {
|
||||
"picker": "Triggers when a tag is scanned (tags are usually created from the Companion app).",
|
||||
"picker": "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}"
|
||||
}
|
||||
@@ -5144,7 +5140,7 @@
|
||||
"value_template": "Value template",
|
||||
"for": "For",
|
||||
"description": {
|
||||
"picker": "Triggers when a template evaluates to true.",
|
||||
"picker": "When a template evaluates to true.",
|
||||
"full": "When a template changes from false to true{hasDuration, select, \n true { for {duration}} \n other {}\n }"
|
||||
}
|
||||
},
|
||||
@@ -5168,7 +5164,7 @@
|
||||
"sun": "[%key:ui::weekdays::sunday%]"
|
||||
},
|
||||
"description": {
|
||||
"picker": "Triggers at a specific time, or on a specific date.",
|
||||
"picker": "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}"
|
||||
}
|
||||
},
|
||||
@@ -5179,7 +5175,7 @@
|
||||
"minutes": "Minutes",
|
||||
"seconds": "Seconds",
|
||||
"description": {
|
||||
"picker": "Triggers periodically, at a defined interval.",
|
||||
"picker": "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}",
|
||||
@@ -5194,7 +5190,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": "Triggers when Home Assistant receives a web request to a webhook endpoint.",
|
||||
"picker": "When Home Assistant receives a web request to the webhook endpoint.",
|
||||
"full": "When a Webhook payload has been received"
|
||||
}
|
||||
},
|
||||
@@ -5206,7 +5202,7 @@
|
||||
"enter": "Enter",
|
||||
"leave": "Leave",
|
||||
"description": {
|
||||
"picker": "Triggers when someone (or something) enters or leaves a zone.",
|
||||
"picker": "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}"
|
||||
}
|
||||
},
|
||||
@@ -8220,8 +8216,7 @@
|
||||
"media_players": "Media players",
|
||||
"other_devices": "Other devices",
|
||||
"weather": "Weather",
|
||||
"energy": "Today's energy",
|
||||
"persons": "People at home"
|
||||
"energy": "Today's energy"
|
||||
},
|
||||
"welcome_user": "Welcome {user}",
|
||||
"summaries": "Summaries",
|
||||
@@ -10705,39 +10700,6 @@
|
||||
"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
src/types/deep-clone-simple.d.ts
vendored
3
src/types/deep-clone-simple.d.ts
vendored
@@ -1,3 +0,0 @@
|
||||
declare module "deep-clone-simple" {
|
||||
export default function deepClone<T>(data: T): T;
|
||||
}
|
||||
@@ -47,11 +47,8 @@ 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,10 +1,7 @@
|
||||
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";
|
||||
@@ -144,84 +141,3 @@ 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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
146
yarn.lock
146
yarn.lock
@@ -4170,12 +4170,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/helpers@npm:0.5.20":
|
||||
version: 0.5.20
|
||||
resolution: "@swc/helpers@npm:0.5.20"
|
||||
"@swc/helpers@npm:0.5.19":
|
||||
version: 0.5.19
|
||||
resolution: "@swc/helpers@npm:0.5.19"
|
||||
dependencies:
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/a46030291484f8fd57505c4ae13cb179aa1f0cef201b14a065d857cfe3c3f41aab46d410a9cec7785f4768ac5b78dc4d07c344086c0ea2cacf67ba034fbed7a2
|
||||
checksum: 10/3fd365fb3265f97e1241bcbcea9bfa5e15e03c630424e1b54597e00d30be2c271cb0c74f45e1739c6bc5ae892647302fab412de5138941aa96e66aebf4586700
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5114,12 +5114,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/coverage-v8@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@vitest/coverage-v8@npm:4.1.2"
|
||||
"@vitest/coverage-v8@npm:4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "@vitest/coverage-v8@npm:4.1.1"
|
||||
dependencies:
|
||||
"@bcoe/v8-coverage": "npm:^1.0.2"
|
||||
"@vitest/utils": "npm:4.1.2"
|
||||
"@vitest/utils": "npm:4.1.1"
|
||||
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.1.0"
|
||||
tinyrainbow: "npm:^3.0.3"
|
||||
peerDependencies:
|
||||
"@vitest/browser": 4.1.2
|
||||
vitest: 4.1.2
|
||||
"@vitest/browser": 4.1.1
|
||||
vitest: 4.1.1
|
||||
peerDependenciesMeta:
|
||||
"@vitest/browser":
|
||||
optional: true
|
||||
checksum: 10/2a38252da937894dfd47a20839714cd49deb8ea0b8289fe25ba17b6677b99dc9b695e4c689b1d6532f19e0d1b81dbac2cf555f82a0ae75abf490dd4107407206
|
||||
checksum: 10/e5873ac0a40fa34772a68f448910cc1d77c97f2c8d1701adcb1d33411d443aa652b851aacbfc99175c3de136fa038eb83f6321321e1dc371999f3dc96999dd69
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/expect@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@vitest/expect@npm:4.1.2"
|
||||
"@vitest/expect@npm:4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "@vitest/expect@npm:4.1.1"
|
||||
dependencies:
|
||||
"@standard-schema/spec": "npm:^1.1.0"
|
||||
"@types/chai": "npm:^5.2.2"
|
||||
"@vitest/spy": "npm:4.1.2"
|
||||
"@vitest/utils": "npm:4.1.2"
|
||||
"@vitest/spy": "npm:4.1.1"
|
||||
"@vitest/utils": "npm:4.1.1"
|
||||
chai: "npm:^6.2.2"
|
||||
tinyrainbow: "npm:^3.1.0"
|
||||
checksum: 10/536c5a8903927e324bbb66967be4e0ec2ec4ff6234f0b8fe20987841b0705c931c7e3ce2e61c7665f4ded65ba736de6cda8d2d37ee114efeedb187ca5d597ea1
|
||||
tinyrainbow: "npm:^3.0.3"
|
||||
checksum: 10/eb74aee01c3c1be58aeb829be6b112600ff34703f2c247ad993db10375d9af87d03c294c485fa6f56754b8af130cc600b397eca081c29d1a2a36b8286e3d0fbd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/mocker@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@vitest/mocker@npm:4.1.2"
|
||||
"@vitest/mocker@npm:4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "@vitest/mocker@npm:4.1.1"
|
||||
dependencies:
|
||||
"@vitest/spy": "npm:4.1.2"
|
||||
"@vitest/spy": "npm:4.1.1"
|
||||
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/1d7976e19ef168357aba2ca41cd8db86236a98dfb2209bd3152a3a20e9a5b8cbfd8f73356c43a934b384d3b4c7a63835fb1037d3f56a7824faa838331eaa214e
|
||||
checksum: 10/76a50f0af8d5e8ffaa29a944be6ac7acade9fd48d9ddd766a0a27ccbeeb80e256845be7f99c383fe235a9edf66d6801ae698505b577060068f181ec25f3e156a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/pretty-format@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@vitest/pretty-format@npm:4.1.2"
|
||||
"@vitest/pretty-format@npm:4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "@vitest/pretty-format@npm:4.1.1"
|
||||
dependencies:
|
||||
tinyrainbow: "npm:^3.1.0"
|
||||
checksum: 10/a07a6023c52b25be5c75fc05bb3317629390cc1b50eae6cbea91ba4c13193ec88e54abaa56b46b40ddb8a6a4558d667f2ba0e1cf2ee2d0e32b463244f3002aa7
|
||||
tinyrainbow: "npm:^3.0.3"
|
||||
checksum: 10/89c260f8361ce11345677ff5f1b99549ad4bde9a38b329885cb20815461b41c7ea6425e4822a7ecdf4ead536cc0dc8757f3a8387f5e9982c741cbb3c8b3eb0cb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/runner@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@vitest/runner@npm:4.1.2"
|
||||
"@vitest/runner@npm:4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "@vitest/runner@npm:4.1.1"
|
||||
dependencies:
|
||||
"@vitest/utils": "npm:4.1.2"
|
||||
"@vitest/utils": "npm:4.1.1"
|
||||
pathe: "npm:^2.0.3"
|
||||
checksum: 10/13fd019a63ee3225420474cbd1ca0ae7c5c2dcdd241f2a958ca45731c10de36131f15303ae8ab1196133ec4e955b7c6de658c7b5e19736d550f310c8195fa9b2
|
||||
checksum: 10/65a4374a4385d2ffccf98110ba7a7521cf9e90ec68b93901f1d5b07f5b574a17fb26e8631d548f703ddaf4ded09b91dc05e4415a986d6e87556689385337c5e7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/snapshot@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@vitest/snapshot@npm:4.1.2"
|
||||
"@vitest/snapshot@npm:4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "@vitest/snapshot@npm:4.1.1"
|
||||
dependencies:
|
||||
"@vitest/pretty-format": "npm:4.1.2"
|
||||
"@vitest/utils": "npm:4.1.2"
|
||||
"@vitest/pretty-format": "npm:4.1.1"
|
||||
"@vitest/utils": "npm:4.1.1"
|
||||
magic-string: "npm:^0.30.21"
|
||||
pathe: "npm:^2.0.3"
|
||||
checksum: 10/9d124412dbe44db43ca5277180bf5fe5dad7373218a177830bba631b53d225f7d4de368a20d6f5740ec07402e9e4dd179609db2b2f691d2d8b02f1bdbfd8c1a3
|
||||
checksum: 10/2f347abfeedb4c2aacad8b0db472842383c5369ed972b65634f327d31c91adc96055426b0dbedcc3275392a57561cffc28d76c526044368fabd1f9ca28e25572
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/spy@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@vitest/spy@npm:4.1.2"
|
||||
checksum: 10/e20e417ac430fee34e4be58802b2eb31e1c1163296a8921c0878be14e1ae77c7a7cae1b9b515d56fe623e05ee21b092aff7eb5e0d412f656650b72ecd02bb30a
|
||||
"@vitest/spy@npm:4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "@vitest/spy@npm:4.1.1"
|
||||
checksum: 10/50dbb99bc4f49b8779dbbe258c3665aa123720560942ec0db18e983abdea9094c54ce137f627eeddc66460f9c6631df273dbf56109d813eca74a85c2188a4548
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/utils@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "@vitest/utils@npm:4.1.2"
|
||||
"@vitest/utils@npm:4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "@vitest/utils@npm:4.1.1"
|
||||
dependencies:
|
||||
"@vitest/pretty-format": "npm:4.1.2"
|
||||
"@vitest/pretty-format": "npm:4.1.1"
|
||||
convert-source-map: "npm:^2.0.0"
|
||||
tinyrainbow: "npm:^3.1.0"
|
||||
checksum: 10/854decf0eb639758d012c9aa53c3d7aed547e37c05ece6704d5f53035be77f704a24973ed95089926e1768c0b55902d42c4438660788e7a0f0e80d0fda1c713b
|
||||
tinyrainbow: "npm:^3.0.3"
|
||||
checksum: 10/11757c339d2942c43ea8bc105a02fda82696cfd78ad70078d4f0784a8699ff8fb811415a73b955291c71fe2b50a822951b42ebcbb2ccc5180fc5757b2eb598a8
|
||||
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.20"
|
||||
"@swc/helpers": "npm:0.5.19"
|
||||
"@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.2"
|
||||
"@vitest/coverage-v8": "npm:4.1.1"
|
||||
"@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.2"
|
||||
vitest: "npm:4.1.1"
|
||||
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.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "tinyrainbow@npm:3.1.0"
|
||||
checksum: 10/4c2c01dde1e5bb9a74973daaae141d4d733d246280b2f9a7f6a9e7dd8e940d48b2580a6086125278777897bc44635d6ccec5f9f563c2179dd2129f4542d0ec05
|
||||
"tinyrainbow@npm:^3.0.3":
|
||||
version: 3.0.3
|
||||
resolution: "tinyrainbow@npm:3.0.3"
|
||||
checksum: 10/169cc63c15e1378674180f3207c82c05bfa58fc79992e48792e8d97b4b759012f48e95297900ede24a81f0087cf329a0d85bb81109739eacf03c650127b3f6c1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -14321,17 +14321,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vitest@npm:4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "vitest@npm:4.1.2"
|
||||
"vitest@npm:4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "vitest@npm:4.1.1"
|
||||
dependencies:
|
||||
"@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"
|
||||
"@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"
|
||||
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.1.0"
|
||||
tinyrainbow: "npm:^3.0.3"
|
||||
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.2
|
||||
"@vitest/browser-preview": 4.1.2
|
||||
"@vitest/browser-webdriverio": 4.1.2
|
||||
"@vitest/ui": 4.1.2
|
||||
"@vitest/browser-playwright": 4.1.1
|
||||
"@vitest/browser-preview": 4.1.1
|
||||
"@vitest/browser-webdriverio": 4.1.1
|
||||
"@vitest/ui": 4.1.1
|
||||
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/6b037387e59d403f6570f887f6ac96b81ff6e768dbd02d32a812ddff5bdebef022dd6d9f20b84fb9535866e0c5dbdf80e6705cc428b6a8f8a8e67e1335235848
|
||||
checksum: 10/2dc81153729c57e6b3ad0aaa920a4f026b717df12741c4c520b8cd49b92fc6e21e9af1ac7568249383521311fcde28f1dfd79a666fd679316016958838249b1b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user