mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-29 02:16:15 +00:00
Compare commits
18
Commits
map-tiles-proxy
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
136cf0c8bf | ||
|
|
920c858056 | ||
|
|
3d2a4bfa04 | ||
|
|
e2c9b2cb79 | ||
|
|
d38ee185bf | ||
|
|
7e10f50419 | ||
|
|
070fd96d6c | ||
|
|
156840b6ff | ||
|
|
5ff7a05ec5 | ||
|
|
c3643555ab | ||
|
|
ae2c296272 | ||
|
|
1db1c8024e | ||
|
|
2f003ca999 | ||
|
|
b1d8a21aaa | ||
|
|
cde2e66229 | ||
|
|
71d5500ed8 | ||
|
|
462321a3c8 | ||
|
|
afc0c36dd9 |
@@ -108,6 +108,10 @@ async function copyMapPanel(staticDir) {
|
||||
// Style, glyphs and sprites for the vector base map
|
||||
await ensureMapAssets();
|
||||
fs.copySync(mapAssetsDir, staticPath("map/"));
|
||||
copyFileDir(
|
||||
npmPath("@mapbox/mapbox-gl-rtl-text/dist/mapbox-gl-rtl-text.js"),
|
||||
staticPath("map/")
|
||||
);
|
||||
}
|
||||
|
||||
function copyZXingWasm(staticDir) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import fs from "fs-extra";
|
||||
import gulp from "gulp";
|
||||
import { extract } from "tar";
|
||||
import paths from "../paths.cjs";
|
||||
import { addLatinLabels } from "./map-labels.js";
|
||||
|
||||
// The tile URL is deliberately never named here: the OSMF asks consumers to
|
||||
// resolve it through the TileJSON so they can move the tiles without every
|
||||
@@ -195,7 +196,7 @@ const buildMapAssets = async () => {
|
||||
["light", colorful],
|
||||
["dark", eclipse],
|
||||
].map(([name, builder]) => {
|
||||
const style = useTileJson(name, builder(styleOptions));
|
||||
const style = addLatinLabels(useTileJson(name, builder(styleOptions)));
|
||||
assertBoldStaysOnRefs(name, style);
|
||||
return writeFile(
|
||||
path.join(outputDir, `${name}.json`),
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Adds the English name to labels whose local name is not in Latin script.
|
||||
// Shortbread tiles carry `name`, `name_en` and `name_de` only.
|
||||
|
||||
const NAME = ["get", "name"];
|
||||
const NAME_EN = ["get", "name_en"];
|
||||
|
||||
// Strings compare by code point: anything from Basic Latin up to Latin
|
||||
// Extended-B, digits and punctuation included.
|
||||
const IS_LATIN = ["<", NAME, "ɐ"];
|
||||
|
||||
const ENGLISH_SCALE = 0.8;
|
||||
|
||||
// Streets are line-placed and cannot break lines.
|
||||
const withEnglish = (placement) =>
|
||||
placement === "line"
|
||||
? ["concat", NAME, " (", NAME_EN, ")"]
|
||||
: ["format", NAME, {}, "\n", {}, NAME_EN, { "font-scale": ENGLISH_SCALE }];
|
||||
|
||||
const isNameLabel = (layer) =>
|
||||
JSON.stringify(layer.layout?.["text-field"]) === JSON.stringify(NAME);
|
||||
|
||||
export const addLatinLabels = (style) => ({
|
||||
...style,
|
||||
layers: style.layers.map((layer) =>
|
||||
isNameLabel(layer)
|
||||
? {
|
||||
...layer,
|
||||
layout: {
|
||||
...layer.layout,
|
||||
"text-field": [
|
||||
"case",
|
||||
IS_LATIN,
|
||||
NAME,
|
||||
["!", ["has", "name_en"]],
|
||||
NAME,
|
||||
withEnglish(layer.layout["symbol-placement"]),
|
||||
],
|
||||
},
|
||||
}
|
||||
: layer
|
||||
),
|
||||
});
|
||||
+7
-6
@@ -75,6 +75,7 @@
|
||||
"@lit/context": "1.1.6",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@lit/task": "1.0.3",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.4.0",
|
||||
"@maplibre/maplibre-gl-leaflet": "0.1.4",
|
||||
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
@@ -109,7 +110,7 @@
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "11.2.14",
|
||||
"js-yaml": "5.3.0",
|
||||
"js-yaml": "5.4.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
@@ -117,7 +118,7 @@
|
||||
"lit-html": "3.3.3",
|
||||
"luxon": "3.7.2",
|
||||
"maplibre-gl": "5.24.0",
|
||||
"marked": "18.0.10",
|
||||
"marked": "18.0.11",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -149,7 +150,7 @@
|
||||
"@eslint/js": "10.0.1",
|
||||
"@gfx/zopfli": "1.0.15",
|
||||
"@html-eslint/eslint-plugin": "0.65.0",
|
||||
"@lokalise/node-api": "16.3.0",
|
||||
"@lokalise/node-api": "16.4.0",
|
||||
"@octokit/auth-oauth-device": "8.0.4",
|
||||
"@octokit/plugin-retry": "8.1.1",
|
||||
"@octokit/rest": "22.0.1",
|
||||
@@ -179,7 +180,7 @@
|
||||
"browserslist": "4.28.8",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.9.0",
|
||||
"eslint": "10.9.1",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import-x": "4.17.1",
|
||||
@@ -189,7 +190,7 @@
|
||||
"eslint-plugin-wc": "3.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.4.0",
|
||||
"generate-license-file": "4.2.1",
|
||||
"generate-license-file": "4.2.3",
|
||||
"glob": "13.0.6",
|
||||
"globals": "17.11.0",
|
||||
"gulp": "5.0.1",
|
||||
@@ -216,7 +217,7 @@
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.67.0",
|
||||
"typescript-eslint": "8.68.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.11",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { maplibreGL } from "@maplibre/maplibre-gl-leaflet";
|
||||
import type { Map as LeafletMap } from "leaflet";
|
||||
import type { StyleSpecification } from "maplibre-gl";
|
||||
import type { setRTLTextPlugin, StyleSpecification } from "maplibre-gl";
|
||||
import type { LeafletModuleType } from "../dom/setup-leaflet-map";
|
||||
|
||||
// Shortbread vector tiles from the OpenStreetMap Foundation. Only their tile
|
||||
@@ -13,6 +13,10 @@ const VECTOR_STYLES = {
|
||||
dark: "/static/map/dark.json",
|
||||
} as const;
|
||||
|
||||
// Without it Arabic and Hebrew labels render reversed. Loaded by MapLibre's
|
||||
// worker, hence a URL rather than an import.
|
||||
const RTL_TEXT_PLUGIN_URL = "/static/map/mapbox-gl-rtl-text.js";
|
||||
|
||||
// Fallback for browsers without WebGL2, which MapLibre needs even for raster,
|
||||
// so it stays a Leaflet tile layer. Still CARTO, and temporarily so: OSM's
|
||||
// raster blocks a browser that sends no Referer, and the only referrer a browser
|
||||
@@ -71,6 +75,20 @@ const loadStyle = async (url: string): Promise<StyleSpecification> => {
|
||||
return style;
|
||||
};
|
||||
|
||||
// Global to MapLibre, and it throws when set twice.
|
||||
let rtlTextPluginRequested = false;
|
||||
const ensureRTLTextPlugin = (setPlugin: typeof setRTLTextPlugin) => {
|
||||
if (rtlTextPluginRequested) {
|
||||
return;
|
||||
}
|
||||
rtlTextPluginRequested = true;
|
||||
setPlugin(new URL(RTL_TEXT_PLUGIN_URL, location.href).href, true).catch(
|
||||
() => {
|
||||
// RTL labels stay reversed; everything else still renders.
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const createVectorLayer = async (
|
||||
createLayer: typeof maplibreGL,
|
||||
leaflet: LeafletModuleType,
|
||||
@@ -211,8 +229,11 @@ export const createBaseLayer = async (
|
||||
if (supportsWebGL2()) {
|
||||
let vectorLayer: MapBaseLayer | undefined;
|
||||
try {
|
||||
const { maplibreGL: createLayer } =
|
||||
await import("@maplibre/maplibre-gl-leaflet");
|
||||
const [{ maplibreGL: createLayer }, maplibre] = await Promise.all([
|
||||
import("@maplibre/maplibre-gl-leaflet"),
|
||||
import("maplibre-gl"),
|
||||
]);
|
||||
ensureRTLTextPlugin(maplibre.setRTLTextPlugin);
|
||||
vectorLayer = await createVectorLayer(
|
||||
createLayer,
|
||||
leaflet,
|
||||
|
||||
+6
-6
@@ -1,12 +1,12 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-nav";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./ha-icon-next";
|
||||
import "./ha-svg-icon";
|
||||
import "./item/ha-list-item-button";
|
||||
import "./list/ha-list-nav";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-config-navigation-list")
|
||||
class HaConfigNavigationList extends LitElement {
|
||||
@@ -95,6 +95,10 @@ export class HaDrawer extends LitElement {
|
||||
}
|
||||
|
||||
private _handleAfterHide(ev: Event) {
|
||||
// Ignore wa-after-hide from nested Web Awesome components (e.g. tooltips)
|
||||
if (ev.target !== ev.currentTarget) {
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
this.open = false;
|
||||
fireEvent(this, "hass-drawer-closed");
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import type { Selector } from "../../data/selector";
|
||||
import {
|
||||
getSelectorInitialValue,
|
||||
getSelectorInitialValueOrUndefined,
|
||||
} from "./get-selector-initial-value";
|
||||
import type { HaFormData, HaFormSchema } from "./types";
|
||||
|
||||
interface ComputeInitialHaFormDataOptions {
|
||||
skipUnsupportedSelectors?: boolean;
|
||||
}
|
||||
|
||||
const setDefaultValue = (
|
||||
field: HaFormSchema,
|
||||
value: HaFormData | undefined
|
||||
@@ -18,7 +25,8 @@ const setDefaultValue = (
|
||||
};
|
||||
|
||||
export const computeInitialHaFormData = (
|
||||
schema: HaFormSchema[] | readonly HaFormSchema[]
|
||||
schema: HaFormSchema[] | readonly HaFormSchema[],
|
||||
options?: ComputeInitialHaFormDataOptions
|
||||
): Record<string, any> => {
|
||||
const data = {};
|
||||
schema.forEach((field) => {
|
||||
@@ -33,7 +41,7 @@ export const computeInitialHaFormData = (
|
||||
} else if ("default" in field) {
|
||||
data[field.name] = setDefaultValue(field, field.default);
|
||||
} else if (field.type === "expandable") {
|
||||
const expandableData = computeInitialHaFormData(field.schema);
|
||||
const expandableData = computeInitialHaFormData(field.schema, options);
|
||||
if (field.required || Object.keys(expandableData).length) {
|
||||
// Only add expandable data if it's required or any of its children have initial values.
|
||||
data[field.name] = expandableData;
|
||||
@@ -62,104 +70,11 @@ export const computeInitialHaFormData = (
|
||||
seconds: 0,
|
||||
};
|
||||
} else if ("selector" in field) {
|
||||
const selector: Selector = field.selector;
|
||||
|
||||
if ("device" in selector) {
|
||||
data[field.name] = selector.device?.multiple ? [] : "";
|
||||
} else if ("entity" in selector) {
|
||||
data[field.name] = selector.entity?.multiple ? [] : "";
|
||||
} else if ("area" in selector) {
|
||||
data[field.name] = selector.area?.multiple ? [] : "";
|
||||
} else if ("label" in selector) {
|
||||
data[field.name] = selector.label?.multiple ? [] : "";
|
||||
} else if ("boolean" in selector) {
|
||||
data[field.name] = false;
|
||||
} else if (
|
||||
"addon" in selector ||
|
||||
"attribute" in selector ||
|
||||
"file" in selector ||
|
||||
"icon" in selector ||
|
||||
"serial_port" in selector ||
|
||||
"template" in selector ||
|
||||
"text" in selector ||
|
||||
"theme" in selector ||
|
||||
"object" in selector
|
||||
) {
|
||||
data[field.name] = "";
|
||||
} else if ("number" in selector) {
|
||||
data[field.name] = selector.number?.min ?? 0;
|
||||
} else if ("select" in selector) {
|
||||
if (selector.select?.options.length) {
|
||||
const firstOption = selector.select.options[0];
|
||||
const val =
|
||||
typeof firstOption === "string" ? firstOption : firstOption.value;
|
||||
data[field.name] = selector.select.multiple ? [val] : val;
|
||||
}
|
||||
} else if ("country" in selector) {
|
||||
if (selector.country?.countries?.length) {
|
||||
data[field.name] = selector.country.countries[0];
|
||||
}
|
||||
} else if ("language" in selector) {
|
||||
if (selector.language?.languages?.length) {
|
||||
data[field.name] = selector.language.languages[0];
|
||||
}
|
||||
} else if ("duration" in selector) {
|
||||
data[field.name] = {
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
};
|
||||
} else if ("time" in selector) {
|
||||
data[field.name] = "00:00:00";
|
||||
} else if ("date" in selector || "datetime" in selector) {
|
||||
const now = new Date().toISOString().slice(0, 10);
|
||||
data[field.name] = `${now}T00:00:00`;
|
||||
} else if ("color_rgb" in selector) {
|
||||
data[field.name] = [0, 0, 0];
|
||||
} else if ("color_temp" in selector) {
|
||||
data[field.name] = selector.color_temp?.min_mireds ?? 153;
|
||||
} else if (
|
||||
"action" in selector ||
|
||||
"trigger" in selector ||
|
||||
"condition" in selector
|
||||
) {
|
||||
data[field.name] = [];
|
||||
} else if ("media" in selector || "target" in selector) {
|
||||
data[field.name] = {};
|
||||
} else if ("state" in selector) {
|
||||
data[field.name] = selector.state?.multiple ? [] : "";
|
||||
} else if ("choose" in selector) {
|
||||
const firstChoice = Object.keys(selector.choose.choices)[0];
|
||||
if (!firstChoice) {
|
||||
data[field.name] = {};
|
||||
} else {
|
||||
data[field.name] = {
|
||||
active_choice: firstChoice,
|
||||
[firstChoice]: computeInitialHaFormData([
|
||||
{
|
||||
name: firstChoice,
|
||||
selector: selector.choose.choices[firstChoice].selector,
|
||||
},
|
||||
])[firstChoice],
|
||||
};
|
||||
}
|
||||
} else if ("numeric_threshold" in selector) {
|
||||
const mode = selector.numeric_threshold?.mode ?? "crossed";
|
||||
const type = mode === "changed" ? "any" : "above";
|
||||
data[field.name] =
|
||||
type === "any"
|
||||
? { type }
|
||||
: {
|
||||
type,
|
||||
value: {
|
||||
number: selector.numeric_threshold?.number?.min ?? 0,
|
||||
active_choice: "number",
|
||||
},
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
`Selector ${Object.keys(selector)[0]} not supported in initial form data`
|
||||
);
|
||||
const initialValue = options?.skipUnsupportedSelectors
|
||||
? getSelectorInitialValueOrUndefined(field.selector)
|
||||
: getSelectorInitialValue(field.selector);
|
||||
if (initialValue !== undefined) {
|
||||
data[field.name] = initialValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,25 +1,89 @@
|
||||
import { DEFAULT_MIN_KELVIN } from "../../common/color/convert-light-color";
|
||||
import type { Selector } from "../../data/selector";
|
||||
import type {
|
||||
Selector,
|
||||
SelectorForType,
|
||||
SelectorType,
|
||||
} from "../../data/selector";
|
||||
|
||||
type SelectorFallbackValues = {
|
||||
[T in SelectorType]: ((selector: SelectorForType<T>) => unknown) | undefined;
|
||||
};
|
||||
|
||||
const SELECTOR_FALLBACK_VALUES = {
|
||||
action: undefined,
|
||||
addon: undefined,
|
||||
automation_behavior: undefined,
|
||||
app: undefined,
|
||||
area: undefined,
|
||||
areas_display: undefined,
|
||||
attribute: undefined,
|
||||
assist_pipeline: undefined,
|
||||
boolean: () => false,
|
||||
choose: undefined,
|
||||
color_rgb: undefined,
|
||||
condition: undefined,
|
||||
config_entry: undefined,
|
||||
conversation_agent: undefined,
|
||||
constant: (selector) => selector.constant?.value,
|
||||
country: undefined,
|
||||
date: undefined,
|
||||
datetime: undefined,
|
||||
device: undefined,
|
||||
device_class: undefined,
|
||||
duration: undefined,
|
||||
entity: undefined,
|
||||
entity_name: undefined,
|
||||
statistic: undefined,
|
||||
file: undefined,
|
||||
floor: undefined,
|
||||
label: undefined,
|
||||
language: undefined,
|
||||
navigation: undefined,
|
||||
number: (selector) => selector.number?.min ?? 0,
|
||||
numeric_threshold: undefined,
|
||||
object: undefined,
|
||||
period: undefined,
|
||||
qr_code: undefined,
|
||||
select: undefined,
|
||||
selector: undefined,
|
||||
serial_port: undefined,
|
||||
state: undefined,
|
||||
backup_location: undefined,
|
||||
stt: undefined,
|
||||
target: undefined,
|
||||
template: undefined,
|
||||
text: undefined,
|
||||
time: undefined,
|
||||
icon: undefined,
|
||||
media: undefined,
|
||||
theme: undefined,
|
||||
timezone: undefined,
|
||||
button_toggle: undefined,
|
||||
trigger: undefined,
|
||||
tts: undefined,
|
||||
tts_voice: undefined,
|
||||
location: undefined,
|
||||
color_temp: (selector) => {
|
||||
if (selector.color_temp?.unit === "kelvin") {
|
||||
return selector.color_temp.min ?? DEFAULT_MIN_KELVIN;
|
||||
}
|
||||
|
||||
return selector.color_temp?.min ?? selector.color_temp?.min_mireds ?? 153;
|
||||
},
|
||||
ui_action: undefined,
|
||||
ui_clock_date_format: undefined,
|
||||
ui_color: undefined,
|
||||
ui_state_content: undefined,
|
||||
ui_time_format: undefined,
|
||||
} satisfies SelectorFallbackValues;
|
||||
|
||||
/**
|
||||
* Value a selector already displays when no field value is set.
|
||||
* Used when enabling an optional service/trigger/condition field.
|
||||
*/
|
||||
export const getSelectorFallbackValue = (selector: Selector): unknown => {
|
||||
if ("constant" in selector) {
|
||||
return selector.constant?.value;
|
||||
}
|
||||
if ("boolean" in selector) {
|
||||
return false;
|
||||
}
|
||||
if ("number" in selector) {
|
||||
return selector.number?.min ?? 0;
|
||||
}
|
||||
if ("color_temp" in selector) {
|
||||
if (selector.color_temp?.unit === "kelvin") {
|
||||
return selector.color_temp.min ?? DEFAULT_MIN_KELVIN;
|
||||
}
|
||||
return selector.color_temp?.min ?? selector.color_temp?.min_mireds ?? 153;
|
||||
}
|
||||
return undefined;
|
||||
const type = Object.keys(selector)[0] as SelectorType;
|
||||
const fallbackValue = SELECTOR_FALLBACK_VALUES[type];
|
||||
|
||||
return fallbackValue?.(selector as never);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import type {
|
||||
Selector,
|
||||
SelectorForType,
|
||||
SelectorType,
|
||||
} from "../../data/selector";
|
||||
|
||||
type SelectorInitialValues = {
|
||||
[T in SelectorType]: ((selector: SelectorForType<T>) => unknown) | undefined;
|
||||
};
|
||||
|
||||
const SELECTOR_INITIAL_VALUES = {
|
||||
action: () => [],
|
||||
addon: () => "",
|
||||
automation_behavior: undefined,
|
||||
app: undefined,
|
||||
area: (selector) => (selector.area?.multiple ? [] : ""),
|
||||
areas_display: undefined,
|
||||
attribute: () => "",
|
||||
assist_pipeline: undefined,
|
||||
boolean: () => false,
|
||||
choose: (selector) => {
|
||||
const firstChoice = Object.keys(selector.choose.choices)[0];
|
||||
|
||||
if (!firstChoice) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const childValue = getSelectorInitialValueOrUndefined(
|
||||
selector.choose.choices[firstChoice].selector
|
||||
);
|
||||
|
||||
return childValue === undefined
|
||||
? { active_choice: firstChoice }
|
||||
: { active_choice: firstChoice, [firstChoice]: childValue };
|
||||
},
|
||||
color_rgb: () => [0, 0, 0],
|
||||
condition: () => [],
|
||||
config_entry: undefined,
|
||||
conversation_agent: undefined,
|
||||
constant: (selector) => selector.constant?.value,
|
||||
country: (selector) => selector.country?.countries?.[0],
|
||||
date: () => `${new Date().toISOString().slice(0, 10)}T00:00:00`,
|
||||
datetime: () => `${new Date().toISOString().slice(0, 10)}T00:00:00`,
|
||||
device: (selector) => (selector.device?.multiple ? [] : ""),
|
||||
device_class: (selector) =>
|
||||
selector.device_class?.multiple ? [] : undefined,
|
||||
duration: () => ({
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
}),
|
||||
entity: (selector) => (selector.entity?.multiple ? [] : ""),
|
||||
entity_name: undefined,
|
||||
statistic: undefined,
|
||||
file: () => "",
|
||||
floor: undefined,
|
||||
label: (selector) => (selector.label?.multiple ? [] : ""),
|
||||
language: (selector) => selector.language?.languages?.[0],
|
||||
navigation: undefined,
|
||||
number: (selector) => selector.number?.min ?? 0,
|
||||
numeric_threshold: (selector) => {
|
||||
const mode = selector.numeric_threshold?.mode ?? "crossed";
|
||||
const type = mode === "changed" ? "any" : "above";
|
||||
|
||||
return type === "any"
|
||||
? { type }
|
||||
: {
|
||||
type,
|
||||
value: {
|
||||
number: selector.numeric_threshold?.number?.min ?? 0,
|
||||
active_choice: "number",
|
||||
},
|
||||
};
|
||||
},
|
||||
object: (selector) => (selector.object?.multiple ? [] : ""),
|
||||
period: undefined,
|
||||
qr_code: undefined,
|
||||
select: (selector) => {
|
||||
const select = selector.select;
|
||||
|
||||
if (!select?.options.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const firstOption = select.options[0];
|
||||
const value =
|
||||
typeof firstOption === "string" ? firstOption : firstOption.value;
|
||||
|
||||
return select.multiple ? [value] : value;
|
||||
},
|
||||
selector: undefined,
|
||||
serial_port: () => "",
|
||||
state: (selector) => (selector.state?.multiple ? [] : ""),
|
||||
backup_location: undefined,
|
||||
stt: undefined,
|
||||
target: () => ({}),
|
||||
template: () => "",
|
||||
text: (selector) => (selector.text?.multiple ? [] : ""),
|
||||
time: () => "00:00:00",
|
||||
icon: () => "",
|
||||
media: () => ({}),
|
||||
theme: () => "",
|
||||
timezone: undefined,
|
||||
button_toggle: undefined,
|
||||
trigger: () => [],
|
||||
tts: undefined,
|
||||
tts_voice: undefined,
|
||||
location: undefined,
|
||||
color_temp: (selector) => selector.color_temp?.min_mireds ?? 153,
|
||||
ui_action: undefined,
|
||||
ui_clock_date_format: undefined,
|
||||
ui_color: undefined,
|
||||
ui_state_content: undefined,
|
||||
ui_time_format: undefined,
|
||||
} satisfies SelectorInitialValues;
|
||||
|
||||
export const getSelectorInitialValueOrUndefined = (
|
||||
selector: Selector
|
||||
): unknown => {
|
||||
const type = Object.keys(selector)[0] as SelectorType;
|
||||
return SELECTOR_INITIAL_VALUES[type]?.(selector as never);
|
||||
};
|
||||
|
||||
export const getSelectorInitialValue = (selector: Selector): unknown => {
|
||||
const type = Object.keys(selector)[0] as SelectorType;
|
||||
const initialValue = SELECTOR_INITIAL_VALUES[type];
|
||||
|
||||
if (!initialValue) {
|
||||
throw new Error(`Selector ${type} not supported in initial form data`);
|
||||
}
|
||||
|
||||
return initialValue(selector as never);
|
||||
};
|
||||
@@ -67,15 +67,16 @@ export class HaNumberSelector extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
// On iOS/iPadOS the numeric and decimal on-screen keypads have no minus key,
|
||||
// so negatives can only be typed with the full "text" keyboard. Other
|
||||
// platforms include a minus on their number keypads, so restrict this
|
||||
// workaround to Safari/WebKit and only when the selector allows negatives
|
||||
// (e.g. numeric_state triggers/conditions).
|
||||
const useTextInputMode =
|
||||
// On iOS/iPadOS the numeric and decimal on-screen keypads have no minus key.
|
||||
// Leaving inputmode unset on a number input gives the "Numbers and
|
||||
// Punctuation" keyboard there, which does include a minus. Other platforms
|
||||
// include a minus on their number keypads, so restrict this workaround to
|
||||
// Safari/WebKit and only when the selector allows negatives: either an
|
||||
// explicit negative min, or no min at all (e.g. the numeric threshold
|
||||
// selector used by the power triggers).
|
||||
const useSafariNegativeKeyboard =
|
||||
isSafari &&
|
||||
this.selector.number?.min !== undefined &&
|
||||
this.selector.number.min < 0;
|
||||
(this.selector.number?.min === undefined || this.selector.number.min < 0);
|
||||
|
||||
const translationKey = this.selector.number?.translation_key;
|
||||
let unit = this.selector.number?.unit_of_measurement;
|
||||
@@ -112,8 +113,8 @@ export class HaNumberSelector extends LitElement {
|
||||
}
|
||||
<ha-input
|
||||
.inputmode=${
|
||||
useTextInputMode
|
||||
? "text"
|
||||
useSafariNegativeKeyboard
|
||||
? undefined
|
||||
: this.selector.number?.step === "any" ||
|
||||
(this.selector.number?.step ?? 1) % 1 !== 0
|
||||
? "decimal"
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { ObjectSelector } from "../../data/selector";
|
||||
import { formatSelectorValue } from "../../data/selector/format_selector_value";
|
||||
import { showFormDialog } from "../../dialogs/form/show-form-dialog";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeInitialHaFormData } from "../ha-form/compute-initial-ha-form-data";
|
||||
import type { HaFormSchema } from "../ha-form/types";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-md-list";
|
||||
@@ -237,10 +238,14 @@ export class HaObjectSelector extends LitElement {
|
||||
private async _addItem(ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
const schema = this._schema(this.selector);
|
||||
|
||||
const newItem = await showFormDialog(this, {
|
||||
title: this.hass.localize("ui.common.add"),
|
||||
schema: this._schema(this.selector),
|
||||
data: {},
|
||||
schema,
|
||||
data: computeInitialHaFormData(schema, {
|
||||
skipUnsupportedSelectors: true,
|
||||
}),
|
||||
computeLabel: this._computeLabel,
|
||||
computeHelper: this._computeHelper,
|
||||
submitText: this.hass.localize("ui.common.add"),
|
||||
@@ -293,7 +298,7 @@ export class HaObjectSelector extends LitElement {
|
||||
const index = ev.currentTarget.index;
|
||||
|
||||
if (!this.selector.object!.multiple) {
|
||||
fireEvent(this, "value-changed", { value: undefined });
|
||||
fireEvent(this, "value-changed", { value: "" });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import type { Selector } from "../../data/selector";
|
||||
import type { Selector, SelectorType } from "../../data/selector";
|
||||
import {
|
||||
handleLegacyDeviceSelector,
|
||||
handleLegacyEntitySelector,
|
||||
@@ -70,7 +70,7 @@ const LOAD_ELEMENTS = {
|
||||
ui_color: () => import("./ha-selector-ui-color"),
|
||||
ui_state_content: () => import("./ha-selector-ui-state-content"),
|
||||
ui_time_format: () => import("./ha-selector-ui-time-format"),
|
||||
};
|
||||
} satisfies Record<SelectorType, () => Promise<unknown>>;
|
||||
|
||||
const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]);
|
||||
|
||||
|
||||
+133
-105
@@ -9,7 +9,8 @@ import {
|
||||
} from "../resources/theme/color/color.globals";
|
||||
import type { HomeAssistant, ThemeSettings, ValueChangedEvent } from "../types";
|
||||
import "./ha-button";
|
||||
import "./ha-settings-row";
|
||||
import "./item/ha-list-item-base";
|
||||
import "./list/ha-list-base";
|
||||
import "./ha-theme-picker";
|
||||
import "./input/ha-input";
|
||||
import "./radio/ha-radio-group";
|
||||
@@ -25,6 +26,7 @@ export interface ThemeSettingsLabels {
|
||||
autoMode?: string;
|
||||
lightMode?: string;
|
||||
darkMode?: string;
|
||||
colors?: string;
|
||||
primaryColor?: string;
|
||||
accentColor?: string;
|
||||
reset?: string;
|
||||
@@ -42,7 +44,7 @@ export class HaThemeSettings extends LitElement {
|
||||
|
||||
@property() public heading?: string;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ attribute: "include-default", type: Boolean })
|
||||
public includeDefault = false;
|
||||
@@ -62,98 +64,121 @@ export class HaThemeSettings extends LitElement {
|
||||
? this.hass.themes.default_dark_theme || this.hass.themes.default_theme
|
||||
: this.hass.themes.default_theme;
|
||||
|
||||
const showDarkMode =
|
||||
curTheme === HOME_ASSISTANT_THEME ||
|
||||
(curThemeIsUseDefault &&
|
||||
this.hass.themes.default_dark_theme &&
|
||||
this.hass.themes.default_theme) ||
|
||||
this._supportsModeSelection(curTheme);
|
||||
|
||||
return html`
|
||||
<ha-settings-row .narrow=${this.narrow} ?empty=${!this.showThemePicker}>
|
||||
${
|
||||
this.heading
|
||||
? html`<span slot="heading">${this.heading}</span>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.description
|
||||
? html`<span slot="description">${this.description}</span>`
|
||||
: nothing
|
||||
}
|
||||
<ha-list-base>
|
||||
${
|
||||
this.showThemePicker
|
||||
? html`
|
||||
<ha-theme-picker
|
||||
.label=${this.labels?.theme}
|
||||
.noThemeLabel=${this.labels?.noTheme}
|
||||
.value=${themeSettings?.theme || undefined}
|
||||
.disabled=${this.themePickerDisabled}
|
||||
?include-default=${this.includeDefault}
|
||||
@value-changed=${this._handleThemeSelection}
|
||||
></ha-theme-picker>
|
||||
<ha-list-item-base>
|
||||
${
|
||||
this.heading
|
||||
? html`<span slot="headline">${this.heading}</span>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.description
|
||||
? html`<span slot="supporting-text"
|
||||
>${this.description}</span
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
<ha-theme-picker
|
||||
slot="end"
|
||||
.label=${this.labels?.theme}
|
||||
.noThemeLabel=${this.labels?.noTheme}
|
||||
.value=${themeSettings?.theme || undefined}
|
||||
.disabled=${this.themePickerDisabled}
|
||||
?include-default=${this.includeDefault}
|
||||
@value-changed=${this._handleThemeSelection}
|
||||
></ha-theme-picker>
|
||||
</ha-list-item-base>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-settings-row>
|
||||
${
|
||||
curTheme === HOME_ASSISTANT_THEME ||
|
||||
(curThemeIsUseDefault &&
|
||||
this.hass.themes.default_dark_theme &&
|
||||
this.hass.themes.default_theme) ||
|
||||
this._supportsModeSelection(curTheme)
|
||||
? html`<div class="inputs">
|
||||
<ha-radio-group
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
.ariaLabel=${this.labels?.mode ?? "Theme mode"}
|
||||
.value=${
|
||||
themeSettings?.dark === undefined
|
||||
? "auto"
|
||||
: themeSettings.dark
|
||||
? "dark"
|
||||
: "light"
|
||||
${
|
||||
showDarkMode
|
||||
? html`
|
||||
<ha-list-item-base>
|
||||
<span slot="headline"
|
||||
>${this.labels?.mode ?? "Theme mode"}</span
|
||||
>
|
||||
<ha-radio-group
|
||||
slot="end"
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
.value=${
|
||||
themeSettings?.dark === undefined
|
||||
? "auto"
|
||||
: themeSettings.dark
|
||||
? "dark"
|
||||
: "light"
|
||||
}
|
||||
orientation="horizontal"
|
||||
>
|
||||
<ha-radio-option value="auto">
|
||||
${this.labels?.autoMode ?? "Auto"}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="light">
|
||||
${this.labels?.lightMode ?? "Light"}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="dark">
|
||||
${this.labels?.darkMode ?? "Dark"}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
</ha-list-item-base>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
curTheme === HOME_ASSISTANT_THEME
|
||||
? html`
|
||||
<ha-list-item-base>
|
||||
<span slot="headline"
|
||||
>${this.labels?.colors ?? "Custom colors"}</span
|
||||
>
|
||||
<div slot="end" class="color-pickers">
|
||||
<ha-input
|
||||
.value=${themeSettings?.primaryColor || DefaultPrimaryColor}
|
||||
type="color"
|
||||
.label=${this.labels?.primaryColor ?? "Primary color"}
|
||||
.name=${"primaryColor"}
|
||||
@change=${this._handleColorChange}
|
||||
></ha-input>
|
||||
<ha-input
|
||||
.value=${themeSettings?.accentColor || DefaultAccentColor}
|
||||
type="color"
|
||||
.label=${this.labels?.accentColor ?? "Accent color"}
|
||||
.name=${"accentColor"}
|
||||
@change=${this._handleColorChange}
|
||||
></ha-input>
|
||||
</div>
|
||||
</ha-list-item-base>
|
||||
${
|
||||
themeSettings?.primaryColor || themeSettings?.accentColor
|
||||
? html`
|
||||
<div class="reset-row">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
@click=${this._resetColors}
|
||||
>
|
||||
${this.labels?.reset ?? "Reset"}
|
||||
</ha-button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
orientation="horizontal"
|
||||
>
|
||||
<ha-radio-option value="auto">
|
||||
${this.labels?.autoMode ?? "Auto"}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="light">
|
||||
${this.labels?.lightMode ?? "Light"}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="dark">
|
||||
${this.labels?.darkMode ?? "Dark"}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
${
|
||||
curTheme === HOME_ASSISTANT_THEME
|
||||
? html`<div class="color-pickers">
|
||||
<ha-input
|
||||
.value=${themeSettings?.primaryColor || DefaultPrimaryColor}
|
||||
type="color"
|
||||
.label=${this.labels?.primaryColor ?? "Primary color"}
|
||||
.name=${"primaryColor"}
|
||||
@change=${this._handleColorChange}
|
||||
></ha-input>
|
||||
<ha-input
|
||||
.value=${themeSettings?.accentColor || DefaultAccentColor}
|
||||
type="color"
|
||||
.label=${this.labels?.accentColor ?? "Accent color"}
|
||||
.name=${"accentColor"}
|
||||
@change=${this._handleColorChange}
|
||||
></ha-input>
|
||||
${
|
||||
themeSettings?.primaryColor ||
|
||||
themeSettings?.accentColor
|
||||
? html` <ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
@click=${this._resetColors}
|
||||
>
|
||||
${this.labels?.reset ?? "Reset"}
|
||||
</ha-button>`
|
||||
: nothing
|
||||
}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-list-base>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -231,33 +256,36 @@ export class HaThemeSettings extends LitElement {
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.inputs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin: 0 var(--ha-space-3);
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-block: var(--ha-space-2);
|
||||
}
|
||||
ha-radio-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-inline-end: var(--ha-space-3);
|
||||
ha-theme-picker {
|
||||
min-width: 150px;
|
||||
}
|
||||
.color-pickers {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
ha-input {
|
||||
min-width: 75px;
|
||||
flex-grow: 1;
|
||||
margin: 0 var(--ha-space-1);
|
||||
.color-pickers ha-input {
|
||||
min-width: 150px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
ha-theme-picker {
|
||||
display: block;
|
||||
:host([narrow]) ha-list-item-base::part(base) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
:host([narrow]) ha-list-item-base::part(end) {
|
||||
width: 100%;
|
||||
}
|
||||
:host([narrow]) .color-pickers {
|
||||
width: 100%;
|
||||
}
|
||||
.reset-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-inline-end: var(--ha-space-4);
|
||||
padding-bottom: var(--ha-space-4);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
+15
-5
@@ -1553,7 +1553,8 @@ export const formatConsumptionShort = (
|
||||
hass: HomeAssistant,
|
||||
consumption: number | null,
|
||||
unit: string,
|
||||
targetUnit?: string
|
||||
targetUnit?: string,
|
||||
displayPrecision?: number
|
||||
): string => {
|
||||
const units = ["Wh", "kWh", "MWh", "GWh", "TWh"];
|
||||
let pickedUnit = unit;
|
||||
@@ -1583,10 +1584,19 @@ export const formatConsumptionShort = (
|
||||
pickedUnit = units[unitIndex];
|
||||
}
|
||||
return (
|
||||
formatNumber(val, hass.locale, {
|
||||
maximumFractionDigits:
|
||||
Math.abs(val) < 10 ? 2 : Math.abs(val) < 100 ? 1 : 0,
|
||||
}) +
|
||||
formatNumber(
|
||||
val,
|
||||
hass.locale,
|
||||
displayPrecision !== undefined && pickedUnit === unit
|
||||
? {
|
||||
minimumFractionDigits: displayPrecision,
|
||||
maximumFractionDigits: displayPrecision,
|
||||
}
|
||||
: {
|
||||
maximumFractionDigits:
|
||||
Math.abs(val) < 10 ? 2 : Math.abs(val) < 100 ? 1 : 0,
|
||||
}
|
||||
) +
|
||||
" " +
|
||||
pickedUnit
|
||||
);
|
||||
|
||||
@@ -50,9 +50,6 @@ export const DOMAIN_ATTRIBUTES_UNITS = {
|
||||
azimuth: "°",
|
||||
elevation: "°",
|
||||
},
|
||||
vacuum: {
|
||||
battery_level: "%",
|
||||
},
|
||||
valve: {
|
||||
current_position: "%",
|
||||
},
|
||||
|
||||
@@ -88,6 +88,20 @@ export type Selector =
|
||||
| UiTimeFormatSelector
|
||||
| BackupLocationSelector;
|
||||
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
export type SelectorType = KeysOfUnion<Selector>;
|
||||
|
||||
type UnionMemberWithKey<U, K extends PropertyKey> = U extends unknown
|
||||
? K extends keyof U
|
||||
? U
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type SelectorForType<T extends SelectorType> = UnionMemberWithKey<
|
||||
Selector,
|
||||
T
|
||||
>;
|
||||
|
||||
export interface ActionSelector {
|
||||
action: {
|
||||
optionsInSidebar?: boolean;
|
||||
|
||||
@@ -116,10 +116,7 @@ export class HaMoreInfoViewVacuumSegmentMapping extends LitElement {
|
||||
></ha-vacuum-segment-area-mapper>
|
||||
|
||||
<div class="footer">
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._dirtyState?.isDirty || this._submitting}
|
||||
>
|
||||
<ha-button @click=${this._save} .disabled=${this._submitting}>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</div>
|
||||
|
||||
@@ -235,7 +235,7 @@ class DialogShortcuts extends DialogMixin(LitElement) {
|
||||
|
||||
<ha-alert slot="footer">
|
||||
${this._i18n.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", {
|
||||
user_profile: html`<a href="/profile/general#shortcuts"
|
||||
user_profile: html`<a href="/profile/browser#shortcuts"
|
||||
>${this._i18n.localize(
|
||||
"ui.dialogs.shortcuts.enable_shortcuts_hint_user_profile"
|
||||
)}</a
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoize from "memoize-one";
|
||||
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
|
||||
import { resolveTimeZone } from "../../common/datetime/resolve-time-zone";
|
||||
@@ -77,6 +78,10 @@ export class HAFullCalendar extends LitElement {
|
||||
|
||||
@property({ attribute: "add-fab", type: Boolean }) public addFab = false;
|
||||
|
||||
@property({ attribute: "add-fab-size" }) public addFabSize = "large";
|
||||
|
||||
@property({ attribute: "add-fab-style" }) public addFabStyle = "on_top";
|
||||
|
||||
@property({ attribute: false }) public events: CalendarEvent[] = [];
|
||||
|
||||
@property({ attribute: false }) public calendars: CalendarData[] = [];
|
||||
@@ -172,13 +177,34 @@ export class HAFullCalendar extends LitElement {
|
||||
</ha-icon-button-next>
|
||||
</div>
|
||||
<h1>${this.calendar.view.title}</h1>
|
||||
<ha-button-toggle-group
|
||||
.buttons=${viewToggleButtons}
|
||||
.active=${this._activeView}
|
||||
size="s"
|
||||
no-wrap
|
||||
@value-changed=${this._handleView}
|
||||
></ha-button-toggle-group>
|
||||
<div>
|
||||
<ha-button-toggle-group
|
||||
.buttons=${viewToggleButtons}
|
||||
.active=${this._activeView}
|
||||
size="s"
|
||||
no-wrap
|
||||
@value-changed=${this._handleView}
|
||||
></ha-button-toggle-group>
|
||||
${
|
||||
this.addFab &&
|
||||
this._hasMutableCalendars &&
|
||||
this.addFabStyle === "header"
|
||||
? html`<ha-button
|
||||
size="s"
|
||||
class="fab-header"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.calendar.event.add"
|
||||
)}
|
||||
@click=${this._createEvent}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot=""
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</ha-button>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="controls">
|
||||
@@ -208,13 +234,34 @@ export class HAFullCalendar extends LitElement {
|
||||
"ui.components.calendar.today"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button-toggle-group
|
||||
.buttons=${viewToggleButtons}
|
||||
.active=${this._activeView}
|
||||
size="s"
|
||||
no-wrap
|
||||
@value-changed=${this._handleView}
|
||||
></ha-button-toggle-group>
|
||||
<div>
|
||||
<ha-button-toggle-group
|
||||
.buttons=${viewToggleButtons}
|
||||
.active=${this._activeView}
|
||||
size="s"
|
||||
no-wrap
|
||||
@value-changed=${this._handleView}
|
||||
></ha-button-toggle-group>
|
||||
${
|
||||
this.addFab &&
|
||||
this._hasMutableCalendars &&
|
||||
this.addFabStyle === "header"
|
||||
? html`<ha-button
|
||||
size="s"
|
||||
class="fab-header"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.calendar.event.add"
|
||||
)}
|
||||
@click=${this._createEvent}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot=""
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</ha-button>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -225,8 +272,15 @@ export class HAFullCalendar extends LitElement {
|
||||
|
||||
<div id="calendar"></div>
|
||||
${
|
||||
this.addFab && this._hasMutableCalendars
|
||||
? html`<ha-button size="l" slot="fab" @click=${this._createEvent}>
|
||||
this.addFab &&
|
||||
this._hasMutableCalendars &&
|
||||
this.addFabStyle !== "header"
|
||||
? html`<ha-button
|
||||
size=${this.addFabSize.charAt(0)}
|
||||
class=${classMap({ below: this.addFabStyle === "below" })}
|
||||
slot="fab"
|
||||
@click=${this._createEvent}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize("ui.components.calendar.event.add")}
|
||||
</ha-button>`
|
||||
@@ -574,6 +628,17 @@ export class HAFullCalendar extends LitElement {
|
||||
--ha-button-box-shadow: var(--ha-box-shadow-l);
|
||||
}
|
||||
|
||||
ha-button.below[slot="fab"] {
|
||||
position: relative;
|
||||
margin-inline-start: auto;
|
||||
padding-top: var(--ha-space-2);
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
--ha-button-box-shadow: none;
|
||||
}
|
||||
|
||||
#calendar {
|
||||
flex-grow: 1;
|
||||
background-color: var(
|
||||
|
||||
@@ -28,7 +28,7 @@ import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart"
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/ha-config-navigation-list";
|
||||
import "../../../components/ha-config-navigation-list";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../config-sections";
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type RegisterChildPanelReady,
|
||||
} from "../../../layouts/panel-ready";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/ha-config-navigation-list";
|
||||
import "../../../components/ha-config-navigation-list";
|
||||
|
||||
@customElement("ha-config-navigation")
|
||||
class HaConfigNavigation extends LitElement {
|
||||
|
||||
@@ -201,7 +201,7 @@ export class DialogVacuumSegmentMapping
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${this._submitting || !this.isDirtyState}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
|
||||
@@ -12,6 +12,15 @@ function getLQIWidth(lqi: number): number {
|
||||
return lqi > 200 ? 3 : lqi > 100 ? 2 : 1;
|
||||
}
|
||||
|
||||
const RELATIONSHIP_PRIORITY: Record<string, number> = {
|
||||
Parent: 0,
|
||||
Sibling: 1,
|
||||
NoneOfTheAbove: 2,
|
||||
Child: 3,
|
||||
PreviousChild: 5,
|
||||
};
|
||||
const UNKNOWN_RELATIONSHIP_PRIORITY = 4;
|
||||
|
||||
export function createZHANetworkChartData(
|
||||
devices: ZHADevice[],
|
||||
hass: HomeAssistant,
|
||||
@@ -159,40 +168,80 @@ export function createZHANetworkChartData(
|
||||
existingLinks.push(link);
|
||||
}
|
||||
});
|
||||
} else if (existingLinks.length === 0) {
|
||||
// If there are no links, create a link to the closest neighbor
|
||||
const neighbors: { ieee: string; lqi: string }[] = device.neighbors ?? [];
|
||||
if (neighbors.length === 0) {
|
||||
// If there are no neighbors, look for links from other devices
|
||||
devices.forEach((d) => {
|
||||
if (d.neighbors && d.neighbors.length > 0) {
|
||||
const neighbor = d.neighbors.find((n) => n.ieee === device.ieee);
|
||||
if (neighbor) {
|
||||
neighbors.push({ ieee: d.ieee, lqi: neighbor.lqi });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
const closestNeighbor = neighbors.sort(
|
||||
(a, b) => parseInt(b.lqi) - parseInt(a.lqi)
|
||||
)[0];
|
||||
if (closestNeighbor) {
|
||||
links.push({
|
||||
source: device.ieee,
|
||||
target: closestNeighbor.ieee,
|
||||
value: parseInt(closestNeighbor.lqi),
|
||||
symbolSize: 5,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
color: style.getPropertyValue("--dark-primary-color"),
|
||||
type: "dotted",
|
||||
},
|
||||
ignoreForceLayout: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// For every device whose routing table was empty (so it got no link
|
||||
// above), independently compute its preferred fallback neighbor before
|
||||
// creating any of these links. Doing this device-by-device while
|
||||
// creating links (as before) meant whichever device the backend happens
|
||||
// to list first "claims" the connection, and a sibling router listed
|
||||
// later never gets to contribute its own — often better — choice,
|
||||
// splitting the graph into islands depending on backend device order.
|
||||
const fallbackTargets = new Map<string, { ieee: string; lqi: string }>();
|
||||
devices.forEach((device) => {
|
||||
const hasLink = links.some(
|
||||
(link) => link.source === device.ieee || link.target === device.ieee
|
||||
);
|
||||
if (hasLink) {
|
||||
return;
|
||||
}
|
||||
const neighbors: { ieee: string; lqi: string; relationship?: string }[] =
|
||||
device.neighbors ?? [];
|
||||
if (neighbors.length === 0) {
|
||||
// If there are no neighbors, look for links from other devices
|
||||
devices.forEach((d) => {
|
||||
if (d.neighbors && d.neighbors.length > 0) {
|
||||
const neighbor = d.neighbors.find((n) => n.ieee === device.ieee);
|
||||
if (neighbor) {
|
||||
neighbors.push({ ieee: d.ieee, lqi: neighbor.lqi });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Prefer a neighbor the device's own Zigbee stack reports as its
|
||||
// parent: picking by raw LQI alone regularly favors a nearby child end
|
||||
// device (e.g. a plug sitting right next to its router) over the
|
||||
// router's actual uplink, which severs it from the rest of the mesh in
|
||||
// the visualization even though the real network is connected.
|
||||
const closestNeighbor = neighbors.sort((a, b) => {
|
||||
const aPriority =
|
||||
RELATIONSHIP_PRIORITY[a.relationship ?? ""] ??
|
||||
UNKNOWN_RELATIONSHIP_PRIORITY;
|
||||
const bPriority =
|
||||
RELATIONSHIP_PRIORITY[b.relationship ?? ""] ??
|
||||
UNKNOWN_RELATIONSHIP_PRIORITY;
|
||||
const priorityDiff = aPriority - bPriority;
|
||||
return priorityDiff !== 0
|
||||
? priorityDiff
|
||||
: parseInt(b.lqi) - parseInt(a.lqi);
|
||||
})[0];
|
||||
if (closestNeighbor) {
|
||||
fallbackTargets.set(device.ieee, closestNeighbor);
|
||||
}
|
||||
});
|
||||
|
||||
const addedFallbackPairs = new Set<string>();
|
||||
fallbackTargets.forEach((target, ieee) => {
|
||||
const pairKey = [ieee, target.ieee].sort().join("|");
|
||||
if (addedFallbackPairs.has(pairKey)) {
|
||||
return;
|
||||
}
|
||||
addedFallbackPairs.add(pairKey);
|
||||
links.push({
|
||||
source: ieee,
|
||||
target: target.ieee,
|
||||
value: parseInt(target.lqi),
|
||||
symbolSize: 5,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
color: style.getPropertyValue("--dark-primary-color"),
|
||||
type: "dotted",
|
||||
},
|
||||
ignoreForceLayout: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Now set ignoreForceLayout to false for the best connection of each device
|
||||
// Except for the coordinator which can have multiple strong connections
|
||||
devices.forEach((device) => {
|
||||
|
||||
@@ -112,7 +112,11 @@ class HuiEnergyDistrubutionCard
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const oldStates = changedProps.get("hass").states;
|
||||
const oldHass = changedProps.get("hass");
|
||||
if (!oldHass) {
|
||||
return true;
|
||||
}
|
||||
const oldStates = oldHass.states;
|
||||
if (
|
||||
this._data?.co2SignalEntity &&
|
||||
this.hass.states[this._data.co2SignalEntity] !==
|
||||
@@ -120,6 +124,20 @@ class HuiEnergyDistrubutionCard
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this._data &&
|
||||
energySourcesByType(this._data.prefs).gas?.some((source) => {
|
||||
const statId = source.stat_energy_from;
|
||||
return (
|
||||
this.hass.entities[statId]?.display_precision !==
|
||||
oldHass.entities[statId]?.display_precision ||
|
||||
this.hass.states[statId]?.attributes.unit_of_measurement !==
|
||||
oldHass.states[statId]?.attributes.unit_of_measurement
|
||||
);
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (this._data && periodIncludesNow(this._data)) {
|
||||
const batteries = energySourcesByType(this._data.prefs).battery;
|
||||
if (
|
||||
@@ -162,6 +180,21 @@ class HuiEnergyDistrubutionCard
|
||||
const hasBattery = types.battery !== undefined;
|
||||
const hasGas = types.gas !== undefined;
|
||||
const hasWater = types.water !== undefined;
|
||||
const gasUnit = this._data.gasUnit;
|
||||
const gasDisplayPrecisions = types.gas
|
||||
?.filter(
|
||||
(source) =>
|
||||
this.hass.states[source.stat_energy_from]?.attributes
|
||||
.unit_of_measurement === gasUnit
|
||||
)
|
||||
.map(
|
||||
(source) =>
|
||||
this.hass.entities[source.stat_energy_from]?.display_precision
|
||||
)
|
||||
.filter((precision): precision is number => precision !== undefined);
|
||||
const gasDisplayPrecision = gasDisplayPrecisions?.length
|
||||
? Math.max(...gasDisplayPrecisions)
|
||||
: undefined;
|
||||
const hasReturnToGrid =
|
||||
types.grid?.some((source) => source.stat_energy_to) ?? false;
|
||||
|
||||
@@ -437,7 +470,9 @@ class HuiEnergyDistrubutionCard
|
||||
${formatConsumptionShort(
|
||||
this.hass,
|
||||
gasUsage,
|
||||
this._data.gasUnit
|
||||
this._data.gasUnit,
|
||||
undefined,
|
||||
gasDisplayPrecision
|
||||
)}
|
||||
</div>
|
||||
<svg width="80" height="30">
|
||||
|
||||
@@ -11,6 +11,7 @@ import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import {
|
||||
energySourcesByType,
|
||||
getEnergyDataCollection,
|
||||
validateEnergyCollectionKey,
|
||||
} from "../../../../data/energy";
|
||||
@@ -66,6 +67,8 @@ export class HuiEnergyGasGraphCard
|
||||
|
||||
@state() private _total?: number;
|
||||
|
||||
private _energyData?: EnergyData;
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
@@ -88,11 +91,68 @@ export class HuiEnergyGasGraphCard
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues<this>): boolean {
|
||||
return (
|
||||
if (
|
||||
hasConfigChanged(this, changedProps) ||
|
||||
changedProps.size > 1 ||
|
||||
!changedProps.has("hass")
|
||||
);
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass");
|
||||
if (!oldHass) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
this._energyData &&
|
||||
energySourcesByType(this._energyData.prefs).gas?.some((source) => {
|
||||
const statId = source.stat_energy_from;
|
||||
return (
|
||||
this.hass.entities[statId]?.display_precision !==
|
||||
oldHass.entities[statId]?.display_precision ||
|
||||
this.hass.states[statId]?.attributes.unit_of_measurement !==
|
||||
oldHass.states[statId]?.attributes.unit_of_measurement
|
||||
);
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private get _displayPrecision(): number | undefined {
|
||||
if (!this._energyData) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const gasDisplayPrecisions = energySourcesByType(this._energyData.prefs)
|
||||
.gas?.filter(
|
||||
(source) =>
|
||||
this.hass.states[source.stat_energy_from]?.attributes
|
||||
.unit_of_measurement === this._unit
|
||||
)
|
||||
.map(
|
||||
(source) =>
|
||||
this.hass.entities[source.stat_energy_from]?.display_precision
|
||||
)
|
||||
.filter((precision): precision is number => precision !== undefined);
|
||||
|
||||
return gasDisplayPrecisions?.length
|
||||
? Math.max(...gasDisplayPrecisions)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private get _gasFormatOptions(): Intl.NumberFormatOptions | undefined {
|
||||
const displayPrecision = this._displayPrecision;
|
||||
|
||||
return displayPrecision !== undefined
|
||||
? {
|
||||
minimumFractionDigits: displayPrecision,
|
||||
maximumFractionDigits: displayPrecision,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -111,7 +171,11 @@ export class HuiEnergyGasGraphCard
|
||||
? html`<hui-energy-graph-chip
|
||||
.tooltip=${this._formatTotal(this._total)}
|
||||
>
|
||||
${formatNumber(this._total, this.hass.locale)}
|
||||
${formatNumber(
|
||||
this._total,
|
||||
this.hass.locale,
|
||||
this._gasFormatOptions
|
||||
)}
|
||||
${this._unit}
|
||||
</hui-energy-graph-chip>`
|
||||
: nothing
|
||||
@@ -135,7 +199,7 @@ export class HuiEnergyGasGraphCard
|
||||
this._unit,
|
||||
this._compareStart,
|
||||
this._compareEnd,
|
||||
this._yAxisFractionDigits
|
||||
this._displayPrecision ?? this._yAxisFractionDigits
|
||||
)}
|
||||
chart-type="bar"
|
||||
></ha-chart-base>
|
||||
@@ -162,7 +226,10 @@ export class HuiEnergyGasGraphCard
|
||||
private _formatTotal = (total: number) =>
|
||||
this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_gas_graph.total_consumed",
|
||||
{ num: formatNumber(total, this.hass.locale), unit: this._unit }
|
||||
{
|
||||
num: formatNumber(total, this.hass.locale, this._gasFormatOptions),
|
||||
unit: this._unit,
|
||||
}
|
||||
);
|
||||
|
||||
private _createOptions = memoizeOne(
|
||||
@@ -191,6 +258,8 @@ export class HuiEnergyGasGraphCard
|
||||
);
|
||||
|
||||
private async _getStatistics(energyData: EnergyData): Promise<void> {
|
||||
this._energyData = energyData;
|
||||
|
||||
const result = generateEnergyGasGraphData({
|
||||
hass: this.hass,
|
||||
energyData,
|
||||
|
||||
@@ -87,11 +87,35 @@ export class HuiEnergySourcesTableCard
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues<this>): boolean {
|
||||
return (
|
||||
if (
|
||||
hasConfigChanged(this, changedProps) ||
|
||||
changedProps.size > 1 ||
|
||||
!changedProps.has("hass")
|
||||
);
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass");
|
||||
if (!oldHass) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
this._data &&
|
||||
energySourcesByType(this._data.prefs).gas?.some((source) => {
|
||||
const statId = source.stat_energy_from;
|
||||
return (
|
||||
this.hass.entities[statId]?.display_precision !==
|
||||
oldHass.entities[statId]?.display_precision ||
|
||||
this.hass.states[statId]?.attributes.unit_of_measurement !==
|
||||
oldHass.states[statId]?.attributes.unit_of_measurement
|
||||
);
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected _renderRow(
|
||||
@@ -108,6 +132,20 @@ export class HuiEnergySourcesTableCard
|
||||
compare: boolean,
|
||||
name?: string
|
||||
) {
|
||||
const displayPrecision =
|
||||
type === "gas" &&
|
||||
this.hass.states[statId]?.attributes.unit_of_measurement === energyUnit
|
||||
? this.hass.entities[statId]?.display_precision
|
||||
: undefined;
|
||||
|
||||
const formatOptions =
|
||||
displayPrecision !== undefined
|
||||
? {
|
||||
minimumFractionDigits: displayPrecision,
|
||||
maximumFractionDigits: displayPrecision,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return html`<tr
|
||||
class="mdc-data-table__row ${classMap({
|
||||
clickable: !isExternalStatistic(statId),
|
||||
@@ -151,7 +189,8 @@ export class HuiEnergySourcesTableCard
|
||||
${
|
||||
compare
|
||||
? html`<td class="mdc-data-table__cell mdc-data-table__cell--numeric">
|
||||
${formatNumber(compareEnergy, this.hass.locale)} ${energyUnit}
|
||||
${formatNumber(compareEnergy, this.hass.locale, formatOptions)}
|
||||
${energyUnit}
|
||||
</td>
|
||||
${
|
||||
showCosts
|
||||
@@ -172,7 +211,7 @@ export class HuiEnergySourcesTableCard
|
||||
: ""
|
||||
}
|
||||
<td class="mdc-data-table__cell mdc-data-table__cell--numeric">
|
||||
${formatNumber(energy, this.hass.locale)} ${energyUnit}
|
||||
${formatNumber(energy, this.hass.locale, formatOptions)} ${energyUnit}
|
||||
</td>
|
||||
${
|
||||
showCosts
|
||||
@@ -203,7 +242,8 @@ export class HuiEnergySourcesTableCard
|
||||
showCosts: boolean,
|
||||
compare: boolean,
|
||||
bulletColor?: { border: string; background: string },
|
||||
isFinalTotal?: boolean
|
||||
isFinalTotal?: boolean,
|
||||
formatOptions?: Intl.NumberFormatOptions
|
||||
) {
|
||||
return html` <tr
|
||||
class="mdc-data-table__row ${bulletColor && !isFinalTotal ? "" : "total"}"
|
||||
@@ -228,7 +268,11 @@ export class HuiEnergySourcesTableCard
|
||||
${
|
||||
compareEnergy === null
|
||||
? ""
|
||||
: `${formatNumber(compareEnergy, this.hass.locale)} ${energyUnit}`
|
||||
: `${formatNumber(
|
||||
compareEnergy,
|
||||
this.hass.locale,
|
||||
formatOptions
|
||||
)} ${energyUnit}`
|
||||
}
|
||||
</td>
|
||||
${
|
||||
@@ -253,7 +297,11 @@ export class HuiEnergySourcesTableCard
|
||||
${
|
||||
energy === null
|
||||
? ""
|
||||
: `${formatNumber(energy, this.hass.locale)} ${energyUnit}`
|
||||
: `${formatNumber(
|
||||
energy,
|
||||
this.hass.locale,
|
||||
formatOptions
|
||||
)} ${energyUnit}`
|
||||
}
|
||||
</td>
|
||||
${
|
||||
@@ -358,6 +406,30 @@ export class HuiEnergySourcesTableCard
|
||||
water: this._data.waterUnit,
|
||||
};
|
||||
|
||||
const gasDisplayPrecisions = types.gas
|
||||
?.filter(
|
||||
(source) =>
|
||||
this.hass.states[source.stat_energy_from]?.attributes
|
||||
.unit_of_measurement === units.gas
|
||||
)
|
||||
.map(
|
||||
(source) =>
|
||||
this.hass.entities[source.stat_energy_from]?.display_precision
|
||||
)
|
||||
.filter((precision): precision is number => precision !== undefined);
|
||||
|
||||
const gasDisplayPrecision = gasDisplayPrecisions?.length
|
||||
? Math.max(...gasDisplayPrecisions)
|
||||
: undefined;
|
||||
|
||||
const gasFormatOptions =
|
||||
gasDisplayPrecision !== undefined
|
||||
? {
|
||||
minimumFractionDigits: gasDisplayPrecision,
|
||||
maximumFractionDigits: gasDisplayPrecision,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const compare = this._data.statsCompare !== undefined;
|
||||
|
||||
const _extractStatData = (
|
||||
@@ -481,7 +553,9 @@ export class HuiEnergySourcesTableCard
|
||||
0
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
: undefined,
|
||||
false,
|
||||
type === "gas" ? gasFormatOptions : undefined
|
||||
)
|
||||
: ""
|
||||
}`;
|
||||
|
||||
@@ -222,6 +222,9 @@ export class HuiCalendarCard
|
||||
"has-title": !!this._config.title,
|
||||
loading: loading,
|
||||
})}
|
||||
?add-fab=${this._config.show_add_event}
|
||||
add-fab-style=${this._config.show_add_event ? (this._config.add_event_style ?? "below") : nothing}
|
||||
add-fab-size=${this._config.show_add_event && this._config.add_event_style !== "header" ? (this._config.add_event_size ?? "small") : nothing}
|
||||
.narrow=${this._narrow}
|
||||
.events=${this._events}
|
||||
.calendars=${this._calendars}
|
||||
@@ -370,11 +373,7 @@ export class HuiCalendarCard
|
||||
}
|
||||
|
||||
private _measureCard() {
|
||||
const card = this.shadowRoot!.querySelector("ha-card");
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
this._narrow = card.offsetWidth < 870;
|
||||
this._narrow = this.offsetWidth < 870;
|
||||
}
|
||||
|
||||
private async _attachObserver(): Promise<void> {
|
||||
@@ -383,12 +382,7 @@ export class HuiCalendarCard
|
||||
debounce(() => this._measureCard(), 250, false)
|
||||
);
|
||||
}
|
||||
const card = this.shadowRoot!.querySelector("ha-card");
|
||||
// If we show an error or warning there is no ha-card
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
this._resizeObserver.observe(card);
|
||||
this._resizeObserver.observe(this);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
@@ -414,9 +408,9 @@ export class HuiCalendarCard
|
||||
|
||||
ha-full-calendar {
|
||||
--calendar-height: 400px;
|
||||
display: block;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: var(--calendar-height);
|
||||
height: 100%;
|
||||
min-height: var(--calendar-height);
|
||||
}
|
||||
|
||||
@@ -431,9 +425,14 @@ export class HuiCalendarCard
|
||||
|
||||
ha-full-calendar.is-grid.has-title,
|
||||
ha-full-calendar.is-panel.has-title {
|
||||
--header-height: calc(
|
||||
var(--ha-card-header-font-size, var(--ha-font-size-2xl)) *
|
||||
var(--ha-line-height-condensed) + 16px
|
||||
);
|
||||
--calendar-height: calc(
|
||||
100% - var(--ha-card-header-font-size, var(--ha-font-size-2xl)) - 22px
|
||||
);
|
||||
height: calc(100% - var(--header-height));
|
||||
}
|
||||
|
||||
.loading {
|
||||
|
||||
@@ -52,6 +52,9 @@ export interface CalendarCardConfig extends LovelaceCardConfig {
|
||||
initial_view?: FullCalendarView;
|
||||
title?: string;
|
||||
theme?: string;
|
||||
show_add_event?: boolean;
|
||||
add_event_style?: "below" | "on_top" | "header";
|
||||
add_event_size?: "small" | "medium" | "large";
|
||||
}
|
||||
|
||||
export interface ConditionalCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -10,12 +10,16 @@ import {
|
||||
optional,
|
||||
string,
|
||||
union,
|
||||
literal,
|
||||
} from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/entity/ha-entities-picker";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { CalendarCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
@@ -27,6 +31,13 @@ const cardConfigStruct = assign(
|
||||
title: optional(union([string(), boolean()])),
|
||||
initial_view: optional(string()),
|
||||
theme: optional(string()),
|
||||
show_add_event: optional(boolean()),
|
||||
add_event_style: optional(
|
||||
union([literal("below"), literal("on_top"), literal("header")])
|
||||
),
|
||||
add_event_size: optional(
|
||||
union([literal("small"), literal("medium"), literal("large")])
|
||||
),
|
||||
entities: array(string()),
|
||||
})
|
||||
);
|
||||
@@ -72,7 +83,83 @@ export class HuiCalendarCardEditor
|
||||
],
|
||||
},
|
||||
{ name: "theme", required: false, selector: { theme: {} } },
|
||||
] as const
|
||||
{ name: "show_add_event", required: false, selector: { boolean: {} } },
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "add_event_style",
|
||||
default: "below",
|
||||
visible: { field: "show_add_event", operator: "eq", value: true },
|
||||
required: false,
|
||||
selector: {
|
||||
select: {
|
||||
options: [
|
||||
{
|
||||
value: "below",
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.card.calendar.add_event.style.below"
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "on_top",
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.card.calendar.add_event.style.on_top"
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "header",
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.card.calendar.add_event.style.header"
|
||||
),
|
||||
},
|
||||
],
|
||||
mode: "dropdown",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add_event_size",
|
||||
default: "small",
|
||||
visible: [
|
||||
{ field: "show_add_event", operator: "eq", value: true },
|
||||
{
|
||||
field: "add_event_style",
|
||||
operator: "not_eq",
|
||||
value: "header",
|
||||
},
|
||||
],
|
||||
required: false,
|
||||
selector: {
|
||||
select: {
|
||||
options: [
|
||||
{
|
||||
value: "small",
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.card.calendar.add_event.size.s"
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "medium",
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.card.calendar.add_event.size.m"
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "large",
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.card.calendar.add_event.size.l"
|
||||
),
|
||||
},
|
||||
],
|
||||
mode: "dropdown",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type {
|
||||
PictureEntityCardConfig,
|
||||
TileCardConfig,
|
||||
} from "../../cards/types";
|
||||
|
||||
export const computeFavoriteCardConfig = (
|
||||
entityId: string
|
||||
): LovelaceCardConfig => {
|
||||
// A camera tile would only show a thumbnail, so give the picture the room.
|
||||
if (computeDomain(entityId) === "camera") {
|
||||
return {
|
||||
type: "picture-entity",
|
||||
entity: entityId,
|
||||
show_name: false,
|
||||
show_state: false,
|
||||
grid_options: {
|
||||
columns: 6,
|
||||
rows: 2,
|
||||
},
|
||||
} satisfies PictureEntityCardConfig;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "tile",
|
||||
entity: entityId,
|
||||
// Favorites come from all over the home, so name the area.
|
||||
state_content: ["state", "area_name"],
|
||||
show_entity_picture: true,
|
||||
} satisfies TileCardConfig;
|
||||
};
|
||||
@@ -34,6 +34,7 @@ import type {
|
||||
TileCardConfig,
|
||||
UpdatesCardConfig,
|
||||
} from "../../cards/types";
|
||||
import { computeFavoriteCardConfig } from "../helpers/favorite-cards";
|
||||
import {
|
||||
LARGE_SCREEN_CONDITION,
|
||||
SMALL_SCREEN_CONDITION,
|
||||
@@ -271,15 +272,7 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
column_span: maxColumns,
|
||||
cards: [
|
||||
favoritesHeadingCard,
|
||||
...favoriteEntities.map(
|
||||
(entityId) =>
|
||||
({
|
||||
type: "tile",
|
||||
entity: entityId,
|
||||
state_content: ["state", "area_name"],
|
||||
show_entity_picture: true,
|
||||
}) satisfies TileCardConfig
|
||||
),
|
||||
...favoriteEntities.map(computeFavoriteCardConfig),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
+6
-10
@@ -4,8 +4,9 @@ import { isComponentLoaded } from "../../../../common/config/is_component_loaded
|
||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import { getCommonControlsUsagePrediction } from "../../../../data/usage_prediction";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { HeadingCardConfig, TileCardConfig } from "../../cards/types";
|
||||
import type { HeadingCardConfig } from "../../cards/types";
|
||||
import type { Condition } from "../../common/validate-condition";
|
||||
import { computeFavoriteCardConfig } from "../helpers/favorite-cards";
|
||||
import type { LovelaceStrategyDependency } from "../types";
|
||||
|
||||
const DEFAULT_LIMIT = 8;
|
||||
@@ -25,13 +26,6 @@ export interface CommonControlsSectionStrategyConfig {
|
||||
title_visibilty?: Condition[];
|
||||
}
|
||||
|
||||
const toTileCard = (entity: string): TileCardConfig => ({
|
||||
type: "tile",
|
||||
entity,
|
||||
state_content: ["state", "area_name"],
|
||||
show_entity_picture: true,
|
||||
});
|
||||
|
||||
@customElement("common-controls-section-strategy")
|
||||
export class CommonControlsSectionStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
|
||||
@@ -63,7 +57,9 @@ export class CommonControlsSectionStrategy extends ReactiveElement {
|
||||
|
||||
// Pinned entities already fill the section, skip the prediction call.
|
||||
if (includedEntities.length >= limit) {
|
||||
section.cards!.push(...includedEntities.slice(0, limit).map(toTileCard));
|
||||
section.cards!.push(
|
||||
...includedEntities.slice(0, limit).map(computeFavoriteCardConfig)
|
||||
);
|
||||
return section;
|
||||
}
|
||||
|
||||
@@ -110,7 +106,7 @@ export class CommonControlsSectionStrategy extends ReactiveElement {
|
||||
return section;
|
||||
}
|
||||
|
||||
section.cards!.push(...entities.map(toTileCard));
|
||||
section.cards!.push(...entities.map(computeFavoriteCardConfig));
|
||||
return section;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,8 @@ import type { HomeAssistant } from "../../../types";
|
||||
import type { TileCardConfig } from "../../lovelace/cards/types";
|
||||
import { BINARY_STATE_ON } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import {
|
||||
type EntityRegistryDisplayEntry,
|
||||
findBatteryChargingEntity,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import { getDeviceEntityDisplayLookup } from "../../../data/device/device_registry";
|
||||
import { findBatteryChargingEntity } from "../../../data/entity/entity_registry";
|
||||
|
||||
export interface MaintenanceViewStrategyConfig {
|
||||
type: "maintenance";
|
||||
@@ -37,44 +35,42 @@ export const maintenanceEntityFilters: EntityFilter[] = [
|
||||
|
||||
const LOW_BATTERY_THRESHOLD = 20;
|
||||
|
||||
const _deviceEntities = memoizeOne(
|
||||
(
|
||||
deviceId: string,
|
||||
entities: HomeAssistant["entities"]
|
||||
): EntityRegistryDisplayEntry[] => {
|
||||
const entries = Object.values(entities);
|
||||
return entries.filter((entity) => entity.device_id === deviceId);
|
||||
}
|
||||
const _deviceEntityLookup = memoizeOne((entities: HomeAssistant["entities"]) =>
|
||||
getDeviceEntityDisplayLookup(Object.values(entities))
|
||||
);
|
||||
|
||||
export const filterLowBatteryEntities = (
|
||||
hass: HomeAssistant,
|
||||
entityIds: string[]
|
||||
): string[] =>
|
||||
entityIds.filter((entityId) => {
|
||||
): string[] => {
|
||||
return entityIds.filter((entityId) => {
|
||||
const state = hass.states[entityId]?.state ?? "";
|
||||
|
||||
if (computeDomain(entityId) === "binary_sensor") {
|
||||
return state === BINARY_STATE_ON;
|
||||
}
|
||||
|
||||
const deviceId = hass.entities[entityId]?.device_id;
|
||||
const entities = deviceId ? _deviceEntities(deviceId, hass.entities) : [];
|
||||
|
||||
const batteryChargingEntity = findBatteryChargingEntity(
|
||||
hass.states,
|
||||
entities
|
||||
);
|
||||
const batteryCharging = batteryChargingEntity
|
||||
? hass.states[batteryChargingEntity?.entity_id]
|
||||
: undefined;
|
||||
|
||||
if (batteryCharging && batteryCharging.state === "on") {
|
||||
const stateValue = parseFloat(state);
|
||||
if (isNaN(stateValue) || stateValue > LOW_BATTERY_THRESHOLD) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stateValue = parseFloat(state);
|
||||
return !isNaN(stateValue) && stateValue <= LOW_BATTERY_THRESHOLD;
|
||||
const deviceId = hass.entities[entityId]?.device_id;
|
||||
if (!deviceId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const batteryChargingEntity = findBatteryChargingEntity(
|
||||
hass.states,
|
||||
_deviceEntityLookup(hass.entities)[deviceId] ?? []
|
||||
);
|
||||
const batteryCharging = batteryChargingEntity
|
||||
? hass.states[batteryChargingEntity.entity_id]
|
||||
: undefined;
|
||||
|
||||
return batteryCharging?.state !== "on";
|
||||
});
|
||||
};
|
||||
|
||||
export const filterUnavailableBatteryEntities = (
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -322,6 +322,15 @@ export const getMyRedirects = (): Redirects => ({
|
||||
profile_security: {
|
||||
redirect: "/profile/security",
|
||||
},
|
||||
profile_preferences: {
|
||||
redirect: "/profile/preferences",
|
||||
},
|
||||
profile_localization: {
|
||||
redirect: "/profile/localization",
|
||||
},
|
||||
profile_browser: {
|
||||
redirect: "/profile/browser",
|
||||
},
|
||||
profile: {
|
||||
redirect: "/profile",
|
||||
},
|
||||
|
||||
@@ -77,7 +77,7 @@ class HaPanelNotFound extends LitElement {
|
||||
),
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/profile/general",
|
||||
navigation_path: "/profile/dashboard",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,38 +1,34 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
import { mdiAccount, mdiLock } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import type { RouterOptions } from "../../layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../layouts/hass-router-page";
|
||||
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
export const profileSections: PageNavigation[] = [
|
||||
{
|
||||
path: "/profile/general",
|
||||
translationKey: "ui.panel.profile.tabs.general",
|
||||
iconPath: mdiAccount,
|
||||
},
|
||||
{
|
||||
path: "/profile/security",
|
||||
translationKey: "ui.panel.profile.tabs.security",
|
||||
iconPath: mdiLock,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("ha-panel-profile")
|
||||
class HaPanelProfile extends SubscribeMixin(HassRouterPage) {
|
||||
class HaPanelProfile extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "general",
|
||||
defaultPage: "dashboard",
|
||||
routes: {
|
||||
general: {
|
||||
tag: "ha-profile-section-general",
|
||||
load: () => import("./ha-profile-section-general"),
|
||||
dashboard: {
|
||||
tag: "ha-profile-dashboard",
|
||||
load: () => import("./ha-profile-dashboard"),
|
||||
},
|
||||
preferences: {
|
||||
tag: "ha-profile-section-preferences",
|
||||
load: () => import("./ha-profile-section-preferences"),
|
||||
},
|
||||
localization: {
|
||||
tag: "ha-profile-section-localization",
|
||||
load: () => import("./ha-profile-section-localization"),
|
||||
},
|
||||
browser: {
|
||||
tag: "ha-profile-section-browser",
|
||||
load: () => import("./ha-profile-section-browser"),
|
||||
},
|
||||
security: {
|
||||
tag: "ha-profile-section-security",
|
||||
|
||||
@@ -81,6 +81,7 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
darkMode: this.hass.localize(
|
||||
"ui.panel.profile.themes.dark_mode.dark"
|
||||
),
|
||||
colors: this.hass.localize("ui.panel.profile.themes.colors"),
|
||||
primaryColor: this.hass.localize(
|
||||
"ui.panel.profile.themes.primary_color"
|
||||
),
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiCellphone,
|
||||
mdiEarth,
|
||||
mdiLock,
|
||||
mdiMonitor,
|
||||
mdiTune,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-config-navigation-list";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "./ha-pick-theme-row";
|
||||
import { isExternal } from "../../data/external";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import "../../layouts/hass-subpage";
|
||||
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../types";
|
||||
|
||||
@customElement("ha-profile-dashboard")
|
||||
class HaProfileDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const pages: PageNavigation[] = [
|
||||
{
|
||||
path: "/profile/preferences",
|
||||
name: this.hass.localize("ui.panel.profile.user_preferences_header"),
|
||||
description: this.hass.localize(
|
||||
"ui.panel.profile.user_preferences_description"
|
||||
),
|
||||
iconPath: mdiTune,
|
||||
},
|
||||
{
|
||||
path: "/profile/localization",
|
||||
name: this.hass.localize("ui.panel.profile.localization_header"),
|
||||
description: this.hass.localize(
|
||||
"ui.panel.profile.localization_description"
|
||||
),
|
||||
iconPath: mdiEarth,
|
||||
},
|
||||
{
|
||||
path: "/profile/browser",
|
||||
name: this.hass.localize(
|
||||
isExternal
|
||||
? "ui.panel.profile.mobile_app_settings"
|
||||
: "ui.panel.profile.browser_settings"
|
||||
),
|
||||
description: this.hass.localize("ui.panel.profile.browser_description"),
|
||||
iconPath: isExternal ? mdiCellphone : mdiMonitor,
|
||||
},
|
||||
{
|
||||
path: "/profile/security",
|
||||
name: this.hass.localize("ui.panel.profile.tabs.security"),
|
||||
description: this.hass.localize(
|
||||
"ui.panel.profile.security_description"
|
||||
),
|
||||
iconPath: mdiLock,
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
main-page
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("panel.profile")}
|
||||
>
|
||||
<div class="container">
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="heading">
|
||||
<div class="icon">
|
||||
<ha-svg-icon .path=${mdiAccount}></ha-svg-icon>
|
||||
</div>
|
||||
<div class="details">
|
||||
${this.hass.user!.name}
|
||||
${
|
||||
this.hass.user!.is_owner
|
||||
? html`<br /><small
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.is_owner"
|
||||
)}</small
|
||||
>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
<ha-button
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
@click=${this._handleLogOut}
|
||||
>
|
||||
${this.hass.localize("ui.panel.profile.logout")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
.header=${this.hass.localize("ui.panel.profile.themes.header")}
|
||||
>
|
||||
<ha-pick-theme-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-theme-row>
|
||||
</ha-card>
|
||||
<ha-card outlined>
|
||||
<ha-config-navigation-list
|
||||
.hass=${this.hass}
|
||||
.pages=${pages}
|
||||
has-secondary
|
||||
.label=${this.hass.localize("panel.profile")}
|
||||
></ha-config-navigation-list>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleLogOut() {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.profile.logout_title"),
|
||||
text: this.hass.localize("ui.panel.profile.logout_text"),
|
||||
confirmText: this.hass.localize("ui.panel.profile.logout"),
|
||||
confirm: () => fireEvent(this, "hass-logout"),
|
||||
destructive: true,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--ha-space-2) var(--ha-space-4)
|
||||
calc(var(--ha-space-4) + var(--safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin: 0 auto var(--ha-space-4);
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
ha-card[outlined] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
border-radius: var(--ha-border-radius-2xl);
|
||||
width: var(--ha-space-10);
|
||||
height: var(--ha-space-10);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon::before {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--primary-color);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.icon ha-svg-icon {
|
||||
color: var(--primary-color);
|
||||
width: var(--ha-space-6);
|
||||
height: var(--ha-space-6);
|
||||
}
|
||||
|
||||
.details {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.details small {
|
||||
font-size: var(--ha-font-size-m);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-button {
|
||||
margin-inline-start: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-profile-dashboard": HaProfileDashboard;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-md-list";
|
||||
import { isExternal } from "../../data/external";
|
||||
import "../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../types";
|
||||
import { isMobileClient } from "../../util/is_mobile";
|
||||
import "./ha-enable-shortcuts-row";
|
||||
import "./ha-force-narrow-row";
|
||||
import "./ha-push-notifications-row";
|
||||
import "./ha-set-suspend-row";
|
||||
import "./ha-set-vibrate-row";
|
||||
|
||||
@customElement("ha-profile-section-browser")
|
||||
class HaProfileSectionBrowser extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/profile"
|
||||
.header=${this.hass.localize(
|
||||
isExternal
|
||||
? "ui.panel.profile.mobile_app_settings"
|
||||
: "ui.panel.profile.browser_settings"
|
||||
)}
|
||||
>
|
||||
<div class="container">
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
isExternal
|
||||
? "ui.panel.profile.mobile_app_settings"
|
||||
: "ui.panel.profile.browser_settings"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize("ui.panel.profile.client_settings_detail")}
|
||||
</div>
|
||||
<ha-md-list>
|
||||
${
|
||||
this.hass.dockedSidebar !== "auto" || !this.narrow
|
||||
? html`
|
||||
<ha-force-narrow-row
|
||||
.hass=${this.hass}
|
||||
></ha-force-narrow-row>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
"vibrate" in navigator
|
||||
? html`
|
||||
<ha-set-vibrate-row
|
||||
.hass=${this.hass}
|
||||
></ha-set-vibrate-row>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
!isExternal &&
|
||||
isComponentLoaded(this.hass.config, "html5.notify")
|
||||
? html`
|
||||
<ha-push-notifications-row
|
||||
.hass=${this.hass}
|
||||
></ha-push-notifications-row>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<ha-set-suspend-row .hass=${this.hass}></ha-set-suspend-row>
|
||||
${
|
||||
!isMobileClient
|
||||
? html`
|
||||
<ha-enable-shortcuts-row
|
||||
id="shortcuts"
|
||||
.hass=${this.hass}
|
||||
></ha-enable-shortcuts-row>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</ha-md-list>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--ha-space-2) var(--ha-space-4)
|
||||
calc(var(--ha-space-4) + var(--safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin: 0 auto var(--ha-space-4);
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-profile-section-browser": HaProfileSectionBrowser;
|
||||
}
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { replaceCurrentUrl } from "../../common/navigate";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/item/ha-row-item";
|
||||
import { isExternal } from "../../data/external";
|
||||
import type { CoreFrontendUserData } from "../../data/frontend";
|
||||
import { subscribeFrontendUserData } from "../../data/frontend";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { showEditSidebarDialog } from "../../dialogs/sidebar/show-dialog-edit-sidebar";
|
||||
import "../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../types";
|
||||
import { isMobileClient } from "../../util/is_mobile";
|
||||
import "./ha-enable-shortcuts-row";
|
||||
import "./ha-entity-id-picker-row";
|
||||
import "./ha-force-narrow-row";
|
||||
import { profileSections } from "./ha-panel-profile";
|
||||
import "./ha-pick-dashboard-row";
|
||||
import "./ha-pick-date-format-row";
|
||||
import "./ha-pick-first-weekday-row";
|
||||
import "./ha-pick-language-row";
|
||||
import "./ha-pick-number-format-row";
|
||||
import "./ha-pick-theme-row";
|
||||
import "./ha-pick-time-format-row";
|
||||
import "./ha-pick-time-zone-row";
|
||||
import "./ha-push-notifications-row";
|
||||
import "./ha-set-suspend-row";
|
||||
import "./ha-set-vibrate-row";
|
||||
|
||||
@customElement("ha-profile-section-general")
|
||||
class HaProfileSectionGeneral extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _coreUserData?: CoreFrontendUserData | null;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
private _unsubCoreData?: Promise<UnsubscribeFunc>;
|
||||
|
||||
private _getCoreData() {
|
||||
this._unsubCoreData = subscribeFrontendUserData(
|
||||
this.hass.connection,
|
||||
"core",
|
||||
({ value }) => {
|
||||
this._coreUserData = value;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hass) {
|
||||
this._getCoreData();
|
||||
}
|
||||
|
||||
this._scrollToHash();
|
||||
}
|
||||
|
||||
public firstUpdated() {
|
||||
if (!this._unsubCoreData) {
|
||||
this._getCoreData();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsubCoreData) {
|
||||
this._unsubCoreData.then((unsub) => unsub());
|
||||
this._unsubCoreData = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async _scrollToHash() {
|
||||
await nextRender();
|
||||
|
||||
const hash = window.location.hash.substring(1);
|
||||
if (hash) {
|
||||
const element = this.shadowRoot!.getElementById(hash);
|
||||
element?.scrollIntoView();
|
||||
this._clearHash();
|
||||
}
|
||||
}
|
||||
|
||||
private _clearHash() {
|
||||
replaceCurrentUrl(window.location.pathname);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
main-page
|
||||
.hass=${this.hass}
|
||||
.tabs=${profileSections}
|
||||
.route=${this.route}
|
||||
>
|
||||
<div slot="title">${this.hass.localize("panel.profile")}</div>
|
||||
<div class="content">
|
||||
<ha-card .header=${this.hass.user!.name}>
|
||||
<div class="card-content">
|
||||
${this.hass.localize("ui.panel.profile.current_user", {
|
||||
fullName: this.hass.user!.name,
|
||||
})}
|
||||
${
|
||||
this.hass.user!.is_owner
|
||||
? this.hass.localize("ui.panel.profile.is_owner")
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
@click=${this._handleLogOut}
|
||||
>
|
||||
${this.hass.localize("ui.panel.profile.logout")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.profile.user_preferences_header"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize("ui.panel.profile.user_preferences_detail")}
|
||||
</div>
|
||||
<ha-pick-language-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-language-row>
|
||||
<ha-pick-theme-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-theme-row>
|
||||
<ha-pick-dashboard-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-dashboard-row>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.header"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.description"
|
||||
)}</span
|
||||
>
|
||||
<ha-button
|
||||
slot="end"
|
||||
appearance="plain"
|
||||
size="s"
|
||||
@click=${this._customizeSidebar}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.button"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
${
|
||||
this.hass.user!.is_admin
|
||||
? html`
|
||||
<ha-entity-id-picker-row
|
||||
.hass=${this.hass}
|
||||
.coreUserData=${this._coreUserData}
|
||||
></ha-entity-id-picker-row>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-card>
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.profile.localization_header"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize("ui.panel.profile.localization_detail")}
|
||||
</div>
|
||||
<ha-pick-time-zone-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-time-zone-row>
|
||||
<ha-pick-number-format-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-number-format-row>
|
||||
<ha-pick-time-format-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-time-format-row>
|
||||
<ha-pick-date-format-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-date-format-row>
|
||||
<ha-pick-first-weekday-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-first-weekday-row>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
isExternal
|
||||
? "ui.panel.profile.mobile_app_settings"
|
||||
: "ui.panel.profile.browser_settings"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize("ui.panel.profile.client_settings_detail")}
|
||||
</div>
|
||||
${
|
||||
this.hass.dockedSidebar !== "auto" || !this.narrow
|
||||
? html`
|
||||
<ha-force-narrow-row
|
||||
.hass=${this.hass}
|
||||
></ha-force-narrow-row>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
"vibrate" in navigator
|
||||
? html`
|
||||
<ha-set-vibrate-row .hass=${this.hass}></ha-set-vibrate-row>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
!isExternal && isComponentLoaded(this.hass.config, "html5.notify")
|
||||
? html`
|
||||
<ha-push-notifications-row
|
||||
.hass=${this.hass}
|
||||
></ha-push-notifications-row>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-set-suspend-row .hass=${this.hass}></ha-set-suspend-row>
|
||||
${
|
||||
!isMobileClient
|
||||
? html`
|
||||
<ha-enable-shortcuts-row
|
||||
id="shortcuts"
|
||||
.hass=${this.hass}
|
||||
></ha-enable-shortcuts-row>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _customizeSidebar() {
|
||||
showEditSidebarDialog(this);
|
||||
}
|
||||
|
||||
private _handleLogOut() {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.profile.logout_title"),
|
||||
text: this.hass.localize("ui.panel.profile.logout_text"),
|
||||
confirmText: this.hass.localize("ui.panel.profile.logout"),
|
||||
confirm: () => fireEvent(this, "hass-logout"),
|
||||
destructive: true,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.content > * {
|
||||
display: block;
|
||||
margin: 24px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-profile-section-general": HaProfileSectionGeneral;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../components/ha-card";
|
||||
import "../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../types";
|
||||
import "./ha-pick-date-format-row";
|
||||
import "./ha-pick-first-weekday-row";
|
||||
import "./ha-pick-language-row";
|
||||
import "./ha-pick-number-format-row";
|
||||
import "./ha-pick-time-format-row";
|
||||
import "./ha-pick-time-zone-row";
|
||||
|
||||
@customElement("ha-profile-section-localization")
|
||||
class HaProfileSectionLocalization extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/profile"
|
||||
.header=${this.hass.localize("ui.panel.profile.localization_header")}
|
||||
>
|
||||
<div class="container">
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.profile.localization_header"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize("ui.panel.profile.localization_detail")}
|
||||
</div>
|
||||
<ha-pick-language-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-language-row>
|
||||
<ha-pick-time-zone-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-time-zone-row>
|
||||
<ha-pick-number-format-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-number-format-row>
|
||||
<ha-pick-time-format-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-time-format-row>
|
||||
<ha-pick-date-format-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-date-format-row>
|
||||
<ha-pick-first-weekday-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-first-weekday-row>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--ha-space-2) var(--ha-space-4)
|
||||
calc(var(--ha-space-4) + var(--safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin: 0 auto var(--ha-space-4);
|
||||
max-width: 600px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-profile-section-localization": HaProfileSectionLocalization;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-md-list";
|
||||
import "../../components/ha-md-list-item";
|
||||
import type { CoreFrontendUserData } from "../../data/frontend";
|
||||
import { subscribeFrontendUserData } from "../../data/frontend";
|
||||
import { showEditSidebarDialog } from "../../dialogs/sidebar/show-dialog-edit-sidebar";
|
||||
import "../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../types";
|
||||
import "./ha-entity-id-picker-row";
|
||||
import "./ha-pick-dashboard-row";
|
||||
|
||||
@customElement("ha-profile-section-preferences")
|
||||
class HaProfileSectionPreferences extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _coreUserData?: CoreFrontendUserData | null;
|
||||
|
||||
private _unsubCoreData?: Promise<UnsubscribeFunc>;
|
||||
|
||||
private _getCoreData() {
|
||||
this._unsubCoreData = subscribeFrontendUserData(
|
||||
this.hass.connection,
|
||||
"core",
|
||||
({ value }) => {
|
||||
this._coreUserData = value;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hass) {
|
||||
this._getCoreData();
|
||||
}
|
||||
}
|
||||
|
||||
public firstUpdated() {
|
||||
if (!this._unsubCoreData) {
|
||||
this._getCoreData();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsubCoreData) {
|
||||
this._unsubCoreData.then((unsub) => unsub());
|
||||
this._unsubCoreData = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/profile"
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.profile.user_preferences_header"
|
||||
)}
|
||||
>
|
||||
<div class="container">
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.profile.user_preferences_header"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize("ui.panel.profile.user_preferences_detail")}
|
||||
</div>
|
||||
<ha-pick-dashboard-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-dashboard-row>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.header"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.description"
|
||||
)}</span
|
||||
>
|
||||
<ha-button
|
||||
slot="end"
|
||||
appearance="plain"
|
||||
size="s"
|
||||
@click=${this._customizeSidebar}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.button"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
${
|
||||
this.hass.user!.is_admin
|
||||
? html`
|
||||
<ha-entity-id-picker-row
|
||||
.hass=${this.hass}
|
||||
.coreUserData=${this._coreUserData}
|
||||
></ha-entity-id-picker-row>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</ha-md-list>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _customizeSidebar() {
|
||||
showEditSidebarDialog(this);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--ha-space-2) var(--ha-space-4)
|
||||
calc(var(--ha-space-4) + var(--safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin: 0 auto var(--ha-space-4);
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-profile-section-preferences": HaProfileSectionPreferences;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../layouts/hass-tabs-subpage";
|
||||
import { profileSections } from "./ha-panel-profile";
|
||||
import "../../layouts/hass-subpage";
|
||||
import type { RefreshToken } from "../../data/refresh_token";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../types";
|
||||
@@ -32,14 +31,12 @@ class HaProfileSectionSecurity extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
main-page
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.tabs=${profileSections}
|
||||
.route=${this.route}
|
||||
back-path="/profile"
|
||||
.header=${this.hass.localize("ui.panel.profile.tabs.security")}
|
||||
>
|
||||
<div slot="title">${this.hass.localize("panel.profile")}</div>
|
||||
<div class="content">
|
||||
<div class="container">
|
||||
${
|
||||
this.hass.user!.credentials.some(
|
||||
(cred) => cred.auth_provider_type === "homeassistant"
|
||||
@@ -70,7 +67,7 @@ class HaProfileSectionSecurity extends LitElement {
|
||||
@hass-refresh-tokens=${this._refreshRefreshTokens}
|
||||
></ha-long-lived-access-tokens-card>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -93,21 +90,15 @@ class HaProfileSectionSecurity extends LitElement {
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
.content {
|
||||
.container {
|
||||
padding: var(--ha-space-2) var(--ha-space-4)
|
||||
calc(var(--ha-space-4) + var(--safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.container > * {
|
||||
display: block;
|
||||
margin: 0 auto var(--ha-space-4);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.content > * {
|
||||
display: block;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.promo-advanced {
|
||||
text-align: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -18,11 +18,9 @@ import type {
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { SecurityAlertEntityConfig } from "../../../data/frontend";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type {
|
||||
LogbookCardConfig,
|
||||
TileCardConfig,
|
||||
} from "../../lovelace/cards/types";
|
||||
import type { LogbookCardConfig } from "../../lovelace/cards/types";
|
||||
import { computeAreaTileCardConfig } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import { computeFavoriteCardConfig } from "../../lovelace/strategies/helpers/favorite-cards";
|
||||
import { computeSecurityAlertCardConfig } from "./security-alerts";
|
||||
|
||||
export interface SecurityViewStrategyConfig {
|
||||
@@ -186,15 +184,7 @@ export class SecurityViewStrategy extends ReactiveElement {
|
||||
),
|
||||
heading_style: "title",
|
||||
},
|
||||
...favoriteEntities.map(
|
||||
(entityId) =>
|
||||
({
|
||||
type: "tile",
|
||||
entity: entityId,
|
||||
state_content: ["state", "area_name"],
|
||||
show_entity_picture: true,
|
||||
}) satisfies TileCardConfig
|
||||
),
|
||||
...favoriteEntities.map(computeFavoriteCardConfig),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9924,6 +9924,21 @@
|
||||
"dayGridWeek": "Week",
|
||||
"dayGridDay": "Day",
|
||||
"listWeek": "List (7 days)"
|
||||
},
|
||||
"show_add_event": "Show add event button",
|
||||
"add_event_style": "Add event button style",
|
||||
"add_event_size": "Add event button size",
|
||||
"add_event": {
|
||||
"style": {
|
||||
"header": "Header",
|
||||
"below": "Below",
|
||||
"on_top": "On top"
|
||||
},
|
||||
"size": {
|
||||
"s": "Small",
|
||||
"m": "Medium",
|
||||
"l": "Large"
|
||||
}
|
||||
}
|
||||
},
|
||||
"conditional": {
|
||||
@@ -11180,17 +11195,19 @@
|
||||
},
|
||||
"profile": {
|
||||
"tabs": {
|
||||
"general": "General",
|
||||
"security": "Security"
|
||||
},
|
||||
"current_user": "You are currently logged in as {fullName}.",
|
||||
"is_owner": "You are an owner.",
|
||||
"user_preferences_header": "User preferences",
|
||||
"user_preferences_header": "Appearance",
|
||||
"user_preferences_description": "Default dashboard and sidebar",
|
||||
"user_preferences_detail": "The following settings are tied to your account and will persist across all sessions and devices.",
|
||||
"localization_header": "Localization",
|
||||
"localization_description": "Language, time zone, date, time, and number formats",
|
||||
"localization_detail": "These settings are tied to your account and control how dates, times, and numbers are displayed.",
|
||||
"mobile_app_settings": "Mobile app settings",
|
||||
"browser_settings": "Browser settings",
|
||||
"mobile_app_settings": "This mobile app",
|
||||
"browser_settings": "This browser",
|
||||
"browser_description": "Notifications, display, and shortcuts",
|
||||
"security_description": "Password, multi-factor authentication, and access tokens",
|
||||
"client_settings_detail": "The following settings are local to this client only, and may reset to defaults on logout or when local data is cleared.",
|
||||
"logout": "Log out",
|
||||
"logout_title": "Log out?",
|
||||
@@ -11305,6 +11322,7 @@
|
||||
"migrate_success": "Theme settings migrated.",
|
||||
"migrate_failed": "Failed to migrate theme settings.",
|
||||
"theme_mode": "Theme mode",
|
||||
"colors": "Custom colors",
|
||||
"dark_mode": {
|
||||
"auto": "Auto",
|
||||
"light": "Light",
|
||||
@@ -11312,7 +11330,7 @@
|
||||
},
|
||||
"primary_color": "Primary color",
|
||||
"accent_color": "Accent color",
|
||||
"reset": "Reset",
|
||||
"reset": "Reset colors",
|
||||
"use_default": "Use default theme"
|
||||
},
|
||||
"dashboard": {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { addLatinLabels } from "../../build-scripts/gulp/map-labels.js";
|
||||
|
||||
// The rewrite keys on the exact `text-field` @versatiles/style emits; a bump
|
||||
// that changed it would silently ship local-only names again.
|
||||
|
||||
const layer = (id, layout) => ({ id, type: "symbol", layout });
|
||||
|
||||
const STYLE = {
|
||||
layers: [
|
||||
layer("label-place-city", { "text-field": ["get", "name"] }),
|
||||
layer("label-street-primary", {
|
||||
"symbol-placement": "line",
|
||||
"text-field": ["get", "name"],
|
||||
}),
|
||||
layer("label-motorway-shield", { "text-field": "{ref}" }),
|
||||
{ id: "water", type: "fill" },
|
||||
],
|
||||
};
|
||||
|
||||
// Just enough of the expression language for the expressions built here.
|
||||
const evaluate = (expression, properties) => {
|
||||
if (!Array.isArray(expression)) {
|
||||
return expression;
|
||||
}
|
||||
const [op, ...args] = expression;
|
||||
switch (op) {
|
||||
case "get":
|
||||
return properties[args[0]];
|
||||
case "has":
|
||||
return args[0] in properties;
|
||||
case "!":
|
||||
return !evaluate(args[0], properties);
|
||||
case "<":
|
||||
return evaluate(args[0], properties) < evaluate(args[1], properties);
|
||||
case "concat":
|
||||
return args.map((arg) => evaluate(arg, properties)).join("");
|
||||
case "format":
|
||||
return args
|
||||
.filter((_, i) => i % 2 === 0)
|
||||
.map((arg) => evaluate(arg, properties))
|
||||
.join("");
|
||||
case "case":
|
||||
for (let i = 0; i < args.length - 1; i += 2) {
|
||||
if (evaluate(args[i], properties)) {
|
||||
return evaluate(args[i + 1], properties);
|
||||
}
|
||||
}
|
||||
return evaluate(args[args.length - 1], properties);
|
||||
default:
|
||||
throw new Error(`Unexpected operator ${op}`);
|
||||
}
|
||||
};
|
||||
|
||||
const textField = (style, id) =>
|
||||
style.layers.find((l) => l.id === id).layout["text-field"];
|
||||
|
||||
describe("addLatinLabels", () => {
|
||||
const style = addLatinLabels(STYLE);
|
||||
const city = textField(style, "label-place-city");
|
||||
const street = textField(style, "label-street-primary");
|
||||
|
||||
it("leaves Latin names alone", () => {
|
||||
expect(evaluate(city, { name: "Köln", name_en: "Cologne" })).toBe("Köln");
|
||||
expect(evaluate(city, { name: "1er arrondissement" })).toBe(
|
||||
"1er arrondissement"
|
||||
);
|
||||
});
|
||||
|
||||
it("adds the English name under a non-Latin one", () => {
|
||||
expect(evaluate(city, { name: "Москва", name_en: "Moscow" })).toBe(
|
||||
"Москва\nMoscow"
|
||||
);
|
||||
expect(evaluate(city, { name: "بيروت", name_en: "Beirut" })).toBe(
|
||||
"بيروت\nBeirut"
|
||||
);
|
||||
expect(evaluate(city, { name: "東京", name_en: "Tokyo" })).toBe(
|
||||
"東京\nTokyo"
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the English line smaller", () => {
|
||||
expect(city.at(-1)).toEqual(
|
||||
expect.arrayContaining([["get", "name_en"], { "font-scale": 0.8 }])
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the local name without an English one", () => {
|
||||
expect(evaluate(city, { name: "בני ברק" })).toBe("בני ברק");
|
||||
});
|
||||
|
||||
it("keeps street labels on one line", () => {
|
||||
expect(
|
||||
evaluate(street, { name: "شارع الحمرا", name_en: "Hamra Street" })
|
||||
).toBe("شارع الحمرا (Hamra Street)");
|
||||
});
|
||||
|
||||
it("does not touch other layers", () => {
|
||||
expect(textField(style, "label-motorway-shield")).toBe("{ref}");
|
||||
expect(style.layers[3]).toEqual(STYLE.layers[3]);
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,10 @@ const maplibreGL = vi.hoisted(() => vi.fn(() => maplibreLayer));
|
||||
|
||||
vi.mock("@maplibre/maplibre-gl-leaflet", () => ({ maplibreGL }));
|
||||
|
||||
const setRTLTextPlugin = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
|
||||
vi.mock("maplibre-gl", () => ({ setRTLTextPlugin }));
|
||||
|
||||
const STYLE = {
|
||||
version: 8,
|
||||
sources: {},
|
||||
@@ -109,6 +113,18 @@ describe("createBaseLayer", () => {
|
||||
browser.retina = false;
|
||||
});
|
||||
|
||||
it("registers the RTL text plugin once, lazily, from our own host", async () => {
|
||||
const createBaseLayer = await setWebGL2(true);
|
||||
await createBaseLayer(leaflet, map, false);
|
||||
await createBaseLayer(leaflet, map, false);
|
||||
|
||||
expect(setRTLTextPlugin).toHaveBeenCalledOnce();
|
||||
expect(setRTLTextPlugin).toHaveBeenCalledWith(
|
||||
`${location.origin}/static/map/mapbox-gl-rtl-text.js`,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("uses vector tiles when WebGL2 is available", async () => {
|
||||
const createBaseLayer = await setWebGL2(true);
|
||||
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { computeInitialHaFormData } from "../../../src/components/ha-form/compute-initial-ha-form-data";
|
||||
import type { Selector } from "../../../src/data/selector";
|
||||
import type { HaFormSchema } from "../../../src/components/ha-form/types";
|
||||
|
||||
const requiredField = (selector: Selector): HaFormSchema => ({
|
||||
name: "value",
|
||||
required: true,
|
||||
selector,
|
||||
});
|
||||
|
||||
const optionalField = (selector: Selector): HaFormSchema => ({
|
||||
name: "value",
|
||||
required: false,
|
||||
selector,
|
||||
});
|
||||
|
||||
describe("computeInitialHaFormData", () => {
|
||||
it("initializes required text selector with an empty string", () => {
|
||||
expect(computeInitialHaFormData([requiredField({ text: {} })])).toEqual({
|
||||
value: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("initializes required multiple text selector with an empty array", () => {
|
||||
expect(
|
||||
computeInitialHaFormData([
|
||||
requiredField({
|
||||
text: {
|
||||
multiple: true,
|
||||
},
|
||||
}),
|
||||
])
|
||||
).toEqual({
|
||||
value: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("initializes required object selector with an empty string", () => {
|
||||
expect(computeInitialHaFormData([requiredField({ object: {} })])).toEqual({
|
||||
value: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("initializes required multiple object selector with an empty array", () => {
|
||||
expect(
|
||||
computeInitialHaFormData([
|
||||
requiredField({
|
||||
object: {
|
||||
multiple: true,
|
||||
},
|
||||
}),
|
||||
])
|
||||
).toEqual({
|
||||
value: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves a required single device class selector unset", () => {
|
||||
expect(
|
||||
computeInitialHaFormData([
|
||||
requiredField({
|
||||
device_class: {
|
||||
domain: "sensor",
|
||||
},
|
||||
}),
|
||||
])
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
it("initializes a required multiple device class selector with an empty array", () => {
|
||||
expect(
|
||||
computeInitialHaFormData([
|
||||
requiredField({
|
||||
device_class: {
|
||||
domain: "sensor",
|
||||
multiple: true,
|
||||
},
|
||||
}),
|
||||
])
|
||||
).toEqual({
|
||||
value: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not initialize optional text selectors", () => {
|
||||
expect(
|
||||
computeInitialHaFormData([
|
||||
optionalField({ text: {} }),
|
||||
{
|
||||
...optionalField({
|
||||
text: {
|
||||
multiple: true,
|
||||
},
|
||||
}),
|
||||
name: "multiple",
|
||||
},
|
||||
])
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
it("does not initialize optional object selectors", () => {
|
||||
expect(
|
||||
computeInitialHaFormData([
|
||||
optionalField({ object: {} }),
|
||||
{
|
||||
...optionalField({
|
||||
object: {
|
||||
multiple: true,
|
||||
},
|
||||
}),
|
||||
name: "multiple",
|
||||
},
|
||||
])
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
it("initializes a required constant selector without losing falsy values", () => {
|
||||
expect(
|
||||
computeInitialHaFormData([
|
||||
requiredField({
|
||||
constant: {
|
||||
value: false,
|
||||
},
|
||||
}),
|
||||
])
|
||||
).toEqual({
|
||||
value: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
computeInitialHaFormData([
|
||||
requiredField({
|
||||
constant: {
|
||||
value: 0,
|
||||
},
|
||||
}),
|
||||
])
|
||||
).toEqual({
|
||||
value: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("initializes a required choose selector from a constant first choice", () => {
|
||||
const schema = [
|
||||
{
|
||||
name: "match",
|
||||
required: true,
|
||||
selector: {
|
||||
choose: {
|
||||
choices: {
|
||||
Disabled: {
|
||||
selector: {
|
||||
constant: {
|
||||
value: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Enabled: {
|
||||
selector: {
|
||||
number: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
expect(computeInitialHaFormData(schema)).toEqual({
|
||||
match: {
|
||||
active_choice: "Disabled",
|
||||
Disabled: "",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("initializes a required choose selector from its child selector", () => {
|
||||
const schema = [
|
||||
{
|
||||
name: "mode",
|
||||
required: true,
|
||||
selector: {
|
||||
choose: {
|
||||
choices: {
|
||||
First: {
|
||||
selector: {
|
||||
text: {
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Second: {
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
expect(computeInitialHaFormData(schema)).toEqual({
|
||||
mode: {
|
||||
active_choice: "First",
|
||||
First: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("throws for an unsupported required selector", () => {
|
||||
expect(() =>
|
||||
computeInitialHaFormData([
|
||||
requiredField({
|
||||
ui_action: {
|
||||
default_action: "none",
|
||||
},
|
||||
}),
|
||||
])
|
||||
).toThrow("Selector ui_action not supported in initial form data");
|
||||
});
|
||||
|
||||
it("omits a first choose child without an initial value", () => {
|
||||
const schema = [
|
||||
{
|
||||
name: "mode",
|
||||
required: true,
|
||||
selector: {
|
||||
choose: {
|
||||
choices: {
|
||||
First: {
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
Second: {
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
expect(computeInitialHaFormData(schema)).toStrictEqual({
|
||||
mode: {
|
||||
active_choice: "First",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,11 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ObjectSelector } from "../../../src/data/selector";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import type { HaObjectSelector } from "../../../src/components/ha-selector/ha-selector-object";
|
||||
import type { FormDialogParams } from "../../../src/dialogs/form/show-form-dialog";
|
||||
import type {
|
||||
FormDialogData,
|
||||
FormDialogParams,
|
||||
} from "../../../src/dialogs/form/show-form-dialog";
|
||||
import "../../../src/components/ha-selector/ha-selector-object";
|
||||
|
||||
vi.mock("../../../src/components/ha-input-helper-text", () => {
|
||||
@@ -51,12 +55,23 @@ const selectorConfig = {
|
||||
const getInternals = (selector: HaObjectSelector) =>
|
||||
selector as unknown as Record<string, unknown>;
|
||||
|
||||
const mountSelector = async (value: Record<string, string>[]) => {
|
||||
interface ItemActionEvent {
|
||||
stopPropagation: () => void;
|
||||
currentTarget: {
|
||||
item?: FormDialogData;
|
||||
index?: number;
|
||||
};
|
||||
}
|
||||
|
||||
const mountSelector = async (
|
||||
value: FormDialogData | FormDialogData[] | "",
|
||||
config: ObjectSelector = selectorConfig
|
||||
) => {
|
||||
const selector = document.createElement(
|
||||
"ha-selector-object"
|
||||
) as HaObjectSelector;
|
||||
selector.hass = hass;
|
||||
selector.selector = selectorConfig;
|
||||
selector.selector = config;
|
||||
selector.value = value;
|
||||
document.body.append(selector);
|
||||
await selector.updateComplete;
|
||||
@@ -66,8 +81,8 @@ const mountSelector = async (value: Record<string, string>[]) => {
|
||||
const resolveFormDialog = async (
|
||||
selector: HaObjectSelector,
|
||||
action: "_addItem" | "_editItem",
|
||||
result: Record<string, string> | null,
|
||||
item?: Record<string, string>,
|
||||
result: FormDialogData | null,
|
||||
item?: FormDialogData,
|
||||
index?: number
|
||||
) => {
|
||||
let params: FormDialogParams | undefined;
|
||||
@@ -87,15 +102,14 @@ const resolveFormDialog = async (
|
||||
);
|
||||
});
|
||||
|
||||
const event = {
|
||||
const event: ItemActionEvent = {
|
||||
stopPropagation: vi.fn(),
|
||||
currentTarget: { item, index },
|
||||
};
|
||||
const operation = (
|
||||
getInternals(selector)[action] as (ev: typeof event) => Promise<void>
|
||||
getInternals(selector)[action] as (event: ItemActionEvent) => Promise<void>
|
||||
)(event);
|
||||
await dialogShown;
|
||||
await operation;
|
||||
await Promise.all([dialogShown, operation]);
|
||||
return params!;
|
||||
};
|
||||
|
||||
@@ -105,6 +119,105 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("ha-selector-object form dialog flow", () => {
|
||||
it("initializes Add dialog data from the object field schema", async () => {
|
||||
const selector = await mountSelector([], {
|
||||
object: {
|
||||
multiple: true,
|
||||
fields: {
|
||||
name: {
|
||||
required: true,
|
||||
selector: { text: {} },
|
||||
},
|
||||
states: {
|
||||
required: true,
|
||||
selector: {
|
||||
text: {
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const params = await resolveFormDialog(selector, "_addItem", {
|
||||
name: "A",
|
||||
states: ["on"],
|
||||
});
|
||||
|
||||
expect(params.data).toEqual({
|
||||
name: "",
|
||||
states: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves unsupported required fields unset in Add dialog data", async () => {
|
||||
const selector = await mountSelector([], {
|
||||
object: {
|
||||
multiple: true,
|
||||
fields: {
|
||||
name: {
|
||||
required: true,
|
||||
selector: { text: {} },
|
||||
},
|
||||
tap_action: {
|
||||
required: true,
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const params = await resolveFormDialog(selector, "_addItem", {
|
||||
name: "A",
|
||||
tap_action: {
|
||||
action: "none",
|
||||
},
|
||||
});
|
||||
|
||||
expect(params.data).toEqual({
|
||||
name: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("restores the initialized empty value after an add-delete round trip", async () => {
|
||||
const selector = await mountSelector("", {
|
||||
object: {
|
||||
fields: {
|
||||
name: {
|
||||
selector: { text: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const valueChanged = vi.fn();
|
||||
selector.addEventListener("value-changed", valueChanged);
|
||||
|
||||
await resolveFormDialog(selector, "_addItem", { name: "A" });
|
||||
|
||||
expect(valueChanged.mock.calls[0][0].detail.value).toEqual({ name: "A" });
|
||||
|
||||
selector.value = valueChanged.mock.calls[0][0].detail.value;
|
||||
await selector.updateComplete;
|
||||
|
||||
const event: ItemActionEvent = {
|
||||
stopPropagation: vi.fn(),
|
||||
currentTarget: {
|
||||
index: 0,
|
||||
},
|
||||
};
|
||||
|
||||
(getInternals(selector)._deleteItem as (event: ItemActionEvent) => void)(
|
||||
event
|
||||
);
|
||||
|
||||
expect(valueChanged.mock.calls[1][0].detail.value).toBe("");
|
||||
});
|
||||
|
||||
it("appends an item through the real Add flow", async () => {
|
||||
const first = { name: "A" };
|
||||
const selector = await mountSelector([first]);
|
||||
|
||||
@@ -101,7 +101,7 @@ test.describe("Home Assistant Demo", () => {
|
||||
);
|
||||
}, DEMO_THEME_STORAGE_KEY);
|
||||
|
||||
await loadDemo(page, "/#/profile/general");
|
||||
await loadDemo(page, "/#/profile/dashboard");
|
||||
|
||||
const themeRow = page.locator("ha-pick-theme-row");
|
||||
await expect(themeRow).toBeVisible({ timeout: PANEL_TIMEOUT });
|
||||
@@ -121,7 +121,7 @@ test.describe("Home Assistant Demo", () => {
|
||||
await expectStoredDemoTheme(page, { theme: "default", dark: true });
|
||||
await expectDemoDarkMode(page, true);
|
||||
|
||||
await loadDemo(page, "/#/profile/general");
|
||||
await loadDemo(page, "/#/profile/dashboard");
|
||||
await expect(themeRow).toBeVisible({ timeout: PANEL_TIMEOUT });
|
||||
await expect(
|
||||
themeRow.locator('ha-radio-option[value="dark"]')
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createZHANetworkChartData } from "../../../../../../src/panels/config/integrations/integration-panels/zha/zha-network-data";
|
||||
import type { ZHADevice } from "../../../../../../src/data/zha";
|
||||
import type { HomeAssistant } from "../../../../../../src/types";
|
||||
|
||||
const hass = {
|
||||
devices: {},
|
||||
areas: {},
|
||||
localize: () => "",
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const device = (partial: Partial<ZHADevice>): ZHADevice =>
|
||||
({
|
||||
available: true,
|
||||
name: partial.ieee!,
|
||||
lqi: 0,
|
||||
rssi: "",
|
||||
last_seen: "",
|
||||
manufacturer: "",
|
||||
model: "",
|
||||
quirk_applied: false,
|
||||
quirk_class: "",
|
||||
entities: [],
|
||||
manufacturer_code: 0,
|
||||
device_reg_id: partial.ieee!,
|
||||
active_coordinator: false,
|
||||
signature: {},
|
||||
routes: [],
|
||||
neighbors: [],
|
||||
...partial,
|
||||
}) as ZHADevice;
|
||||
|
||||
describe("createZHANetworkChartData", () => {
|
||||
it("links a router to its upstream router instead of a nearby child device when routing tables are empty", () => {
|
||||
// Regression test: many Zigbee radios (e.g. TI CC2652 via zigpy-znp)
|
||||
// never populate Mgmt_Rtg routes, so the chart has to fall back to
|
||||
// picking a device's strongest RF neighbor. A router's own child end
|
||||
// device (e.g. a plug sitting right next to it) commonly reports a
|
||||
// stronger LQI than the router's real uplink, which must not be
|
||||
// allowed to hide the backbone connection.
|
||||
const coordinator = device({
|
||||
ieee: "coordinator",
|
||||
device_type: "Coordinator",
|
||||
nwk: 0,
|
||||
});
|
||||
const upstreamRouter = device({
|
||||
ieee: "upstream-router",
|
||||
device_type: "Router",
|
||||
nwk: 1,
|
||||
neighbors: [
|
||||
{
|
||||
ieee: "coordinator",
|
||||
nwk: "0x0000",
|
||||
lqi: "200",
|
||||
depth: "0",
|
||||
relationship: "Parent",
|
||||
},
|
||||
],
|
||||
});
|
||||
const downstreamRouter = device({
|
||||
ieee: "downstream-router",
|
||||
device_type: "Router",
|
||||
nwk: 2,
|
||||
neighbors: [
|
||||
// Real backbone link: weaker signal, correctly flagged as a sibling router
|
||||
{
|
||||
ieee: "upstream-router",
|
||||
nwk: "0x0001",
|
||||
lqi: "80",
|
||||
depth: "1",
|
||||
relationship: "Sibling",
|
||||
},
|
||||
// Nearby child end device with a much stronger signal than the real uplink
|
||||
{
|
||||
ieee: "child-end-device",
|
||||
nwk: "0x0003",
|
||||
lqi: "250",
|
||||
depth: "2",
|
||||
relationship: "Child",
|
||||
},
|
||||
],
|
||||
});
|
||||
const childEndDevice = device({
|
||||
ieee: "child-end-device",
|
||||
device_type: "EndDevice",
|
||||
nwk: 3,
|
||||
neighbors: [
|
||||
{
|
||||
ieee: "downstream-router",
|
||||
nwk: "0x0002",
|
||||
lqi: "250",
|
||||
depth: "2",
|
||||
relationship: "Parent",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { links } = createZHANetworkChartData(
|
||||
[coordinator, upstreamRouter, downstreamRouter, childEndDevice],
|
||||
hass,
|
||||
document.createElement("div")
|
||||
);
|
||||
|
||||
const adjacency = new Map<string, string[]>();
|
||||
for (const link of links) {
|
||||
adjacency.set(link.source, [
|
||||
...(adjacency.get(link.source) ?? []),
|
||||
link.target,
|
||||
]);
|
||||
adjacency.set(link.target, [
|
||||
...(adjacency.get(link.target) ?? []),
|
||||
link.source,
|
||||
]);
|
||||
}
|
||||
|
||||
// Every device should be reachable from the coordinator. Before the
|
||||
// fix, downstream-router picked its child as its only link, leaving
|
||||
// it (and its children) as a disconnected island in the chart even
|
||||
// though the real network is fully connected.
|
||||
const reachable = new Set(["coordinator"]);
|
||||
const queue = ["coordinator"];
|
||||
while (queue.length) {
|
||||
const current = queue.shift()!;
|
||||
for (const neighbor of adjacency.get(current) ?? []) {
|
||||
if (!reachable.has(neighbor)) {
|
||||
reachable.add(neighbor);
|
||||
queue.push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(reachable.has("upstream-router")).toBe(true);
|
||||
expect(reachable.has("downstream-router")).toBe(true);
|
||||
expect(reachable.has("child-end-device")).toBe(true);
|
||||
});
|
||||
|
||||
it("ranks NoneOfTheAbove above Child, and treats unrecognized relationships as better than PreviousChild", () => {
|
||||
const router = device({
|
||||
ieee: "router",
|
||||
device_type: "Router",
|
||||
nwk: 1,
|
||||
neighbors: [
|
||||
{
|
||||
ieee: "n-child",
|
||||
nwk: "0x0001",
|
||||
lqi: "100",
|
||||
depth: "1",
|
||||
relationship: "Child",
|
||||
},
|
||||
{
|
||||
ieee: "n-none-of-the-above",
|
||||
nwk: "0x0002",
|
||||
lqi: "100",
|
||||
depth: "1",
|
||||
relationship: "NoneOfTheAbove",
|
||||
},
|
||||
],
|
||||
});
|
||||
const unknownRelationship = device({
|
||||
ieee: "router-2",
|
||||
device_type: "Router",
|
||||
nwk: 2,
|
||||
neighbors: [
|
||||
{
|
||||
ieee: "n-unrecognized",
|
||||
nwk: "0x0003",
|
||||
lqi: "100",
|
||||
depth: "1",
|
||||
relationship: "SomeFutureValue",
|
||||
},
|
||||
{
|
||||
ieee: "n-previous-child",
|
||||
nwk: "0x0004",
|
||||
lqi: "100",
|
||||
depth: "1",
|
||||
relationship: "PreviousChild",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { links } = createZHANetworkChartData(
|
||||
[router, unknownRelationship],
|
||||
hass,
|
||||
document.createElement("div")
|
||||
);
|
||||
|
||||
const otherEndOf = (ieee: string) => {
|
||||
const link = links.find((l) => l.source === ieee || l.target === ieee);
|
||||
return link?.source === ieee ? link.target : link?.source;
|
||||
};
|
||||
|
||||
expect(otherEndOf("router")).toBe("n-none-of-the-above");
|
||||
expect(otherEndOf("router-2")).toBe("n-unrecognized");
|
||||
});
|
||||
|
||||
it("connects every device regardless of backend device order (child listed before its parent router)", () => {
|
||||
// Regression test for a subtler variant of the same bug: the fallback
|
||||
// link is only computed for a device if it doesn't already have a
|
||||
// link. If the backend lists a child end device before its parent
|
||||
// router, the child claims the link first, and the router - now
|
||||
// appearing to "already have a link" - never gets to evaluate its own
|
||||
// (better) neighbor choice, splitting the graph exactly as before but
|
||||
// triggered by device order instead of by LQI.
|
||||
const coordinator = device({
|
||||
ieee: "coordinator",
|
||||
device_type: "Coordinator",
|
||||
nwk: 0,
|
||||
});
|
||||
const upstreamRouter = device({
|
||||
ieee: "upstream-router",
|
||||
device_type: "Router",
|
||||
nwk: 1,
|
||||
neighbors: [
|
||||
{
|
||||
ieee: "coordinator",
|
||||
nwk: "0x0000",
|
||||
lqi: "200",
|
||||
depth: "0",
|
||||
relationship: "Parent",
|
||||
},
|
||||
],
|
||||
});
|
||||
const downstreamRouter = device({
|
||||
ieee: "downstream-router",
|
||||
device_type: "Router",
|
||||
nwk: 2,
|
||||
neighbors: [
|
||||
{
|
||||
ieee: "upstream-router",
|
||||
nwk: "0x0001",
|
||||
lqi: "80",
|
||||
depth: "1",
|
||||
relationship: "Sibling",
|
||||
},
|
||||
{
|
||||
ieee: "child-end-device",
|
||||
nwk: "0x0003",
|
||||
lqi: "250",
|
||||
depth: "2",
|
||||
relationship: "Child",
|
||||
},
|
||||
],
|
||||
});
|
||||
const childEndDevice = device({
|
||||
ieee: "child-end-device",
|
||||
device_type: "EndDevice",
|
||||
nwk: 3,
|
||||
neighbors: [
|
||||
{
|
||||
ieee: "downstream-router",
|
||||
nwk: "0x0002",
|
||||
lqi: "250",
|
||||
depth: "2",
|
||||
relationship: "Parent",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Same topology as the first test, but the child is listed before its
|
||||
// parent router this time.
|
||||
const { links } = createZHANetworkChartData(
|
||||
[coordinator, upstreamRouter, childEndDevice, downstreamRouter],
|
||||
hass,
|
||||
document.createElement("div")
|
||||
);
|
||||
|
||||
const adjacency = new Map<string, string[]>();
|
||||
for (const link of links) {
|
||||
adjacency.set(link.source, [
|
||||
...(adjacency.get(link.source) ?? []),
|
||||
link.target,
|
||||
]);
|
||||
adjacency.set(link.target, [
|
||||
...(adjacency.get(link.target) ?? []),
|
||||
link.source,
|
||||
]);
|
||||
}
|
||||
|
||||
const reachable = new Set(["coordinator"]);
|
||||
const queue = ["coordinator"];
|
||||
while (queue.length) {
|
||||
const current = queue.shift()!;
|
||||
for (const neighbor of adjacency.get(current) ?? []) {
|
||||
if (!reachable.has(neighbor)) {
|
||||
reachable.add(neighbor);
|
||||
queue.push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(reachable.has("upstream-router")).toBe(true);
|
||||
expect(reachable.has("downstream-router")).toBe(true);
|
||||
expect(reachable.has("child-end-device")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { filterLowBatteryEntities } from "../../../../src/panels/maintenance/strategies/maintenance-view-strategy";
|
||||
import { mockEntity } from "../../../common/entity/context/context-mock";
|
||||
import { createMockEntityState, createMockHass } from "../../../fixtures/hass";
|
||||
|
||||
describe("filterLowBatteryEntities", () => {
|
||||
it("filters numeric battery entities by the low battery threshold", () => {
|
||||
const hass = createMockHass({
|
||||
"sensor.low_battery": createMockEntityState("sensor.low_battery", "20", {
|
||||
device_class: "battery",
|
||||
}),
|
||||
"sensor.ok_battery": createMockEntityState("sensor.ok_battery", "21", {
|
||||
device_class: "battery",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(
|
||||
filterLowBatteryEntities(hass, [
|
||||
"sensor.low_battery",
|
||||
"sensor.ok_battery",
|
||||
])
|
||||
).toEqual(["sensor.low_battery"]);
|
||||
});
|
||||
|
||||
it("excludes a low battery when its device is charging", () => {
|
||||
const entities = {
|
||||
"sensor.device_1_battery": mockEntity({
|
||||
entity_id: "sensor.device_1_battery",
|
||||
device_id: "device_1",
|
||||
}),
|
||||
"binary_sensor.device_1_battery_charging": mockEntity({
|
||||
entity_id: "binary_sensor.device_1_battery_charging",
|
||||
device_id: "device_1",
|
||||
}),
|
||||
"sensor.device_2_battery": mockEntity({
|
||||
entity_id: "sensor.device_2_battery",
|
||||
device_id: "device_2",
|
||||
}),
|
||||
"binary_sensor.device_2_battery_charging": mockEntity({
|
||||
entity_id: "binary_sensor.device_2_battery_charging",
|
||||
device_id: "device_2",
|
||||
}),
|
||||
};
|
||||
|
||||
const hass = createMockHass(
|
||||
{
|
||||
"sensor.device_1_battery": createMockEntityState(
|
||||
"sensor.device_1_battery",
|
||||
"10",
|
||||
{ device_class: "battery" }
|
||||
),
|
||||
"binary_sensor.device_1_battery_charging": createMockEntityState(
|
||||
"binary_sensor.device_1_battery_charging",
|
||||
"on",
|
||||
{ device_class: "battery_charging" }
|
||||
),
|
||||
"sensor.device_2_battery": createMockEntityState(
|
||||
"sensor.device_2_battery",
|
||||
"10",
|
||||
{ device_class: "battery" }
|
||||
),
|
||||
"binary_sensor.device_2_battery_charging": createMockEntityState(
|
||||
"binary_sensor.device_2_battery_charging",
|
||||
"off",
|
||||
{ device_class: "battery_charging" }
|
||||
),
|
||||
},
|
||||
{ entities }
|
||||
);
|
||||
|
||||
expect(
|
||||
filterLowBatteryEntities(hass, [
|
||||
"sensor.device_1_battery",
|
||||
"sensor.device_2_battery",
|
||||
])
|
||||
).toEqual(["sensor.device_2_battery"]);
|
||||
});
|
||||
|
||||
it("keeps binary battery sensor behavior unchanged", () => {
|
||||
const hass = createMockHass({
|
||||
"binary_sensor.low_battery": createMockEntityState(
|
||||
"binary_sensor.low_battery",
|
||||
"on",
|
||||
{ device_class: "battery" }
|
||||
),
|
||||
"binary_sensor.ok_battery": createMockEntityState(
|
||||
"binary_sensor.ok_battery",
|
||||
"off",
|
||||
{ device_class: "battery" }
|
||||
),
|
||||
});
|
||||
|
||||
expect(
|
||||
filterLowBatteryEntities(hass, [
|
||||
"binary_sensor.low_battery",
|
||||
"binary_sensor.ok_battery",
|
||||
])
|
||||
).toEqual(["binary_sensor.low_battery"]);
|
||||
});
|
||||
|
||||
it("updates the device lookup when the entity registry changes", () => {
|
||||
const states = {
|
||||
"sensor.battery": createMockEntityState("sensor.battery", "10", {
|
||||
device_class: "battery",
|
||||
}),
|
||||
"binary_sensor.battery_charging": createMockEntityState(
|
||||
"binary_sensor.battery_charging",
|
||||
"on",
|
||||
{ device_class: "battery_charging" }
|
||||
),
|
||||
};
|
||||
|
||||
const firstHass = createMockHass(states, {
|
||||
entities: {
|
||||
"sensor.battery": mockEntity({
|
||||
entity_id: "sensor.battery",
|
||||
device_id: "device_1",
|
||||
}),
|
||||
"binary_sensor.battery_charging": mockEntity({
|
||||
entity_id: "binary_sensor.battery_charging",
|
||||
device_id: "device_1",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
expect(filterLowBatteryEntities(firstHass, ["sensor.battery"])).toEqual([]);
|
||||
|
||||
const secondHass = createMockHass(states, {
|
||||
entities: {
|
||||
"sensor.battery": mockEntity({
|
||||
entity_id: "sensor.battery",
|
||||
device_id: "device_1",
|
||||
}),
|
||||
"binary_sensor.battery_charging": mockEntity({
|
||||
entity_id: "binary_sensor.battery_charging",
|
||||
device_id: "device_2",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
expect(filterLowBatteryEntities(secondHass, ["sensor.battery"])).toEqual([
|
||||
"sensor.battery",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -3488,10 +3488,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@lokalise/node-api@npm:16.3.0":
|
||||
version: 16.3.0
|
||||
resolution: "@lokalise/node-api@npm:16.3.0"
|
||||
checksum: 10/9902033459cd3abb6dd0ad2846a4c464144caf890433456c2b966500201b6b692f751e9e23e925f968f9501f431ea6bbc07fdfb59f367fd6f3351d6bb7ae67bd
|
||||
"@lokalise/node-api@npm:16.4.0":
|
||||
version: 16.4.0
|
||||
resolution: "@lokalise/node-api@npm:16.4.0"
|
||||
checksum: 10/68bc43e4cdeec93b44ddbfc10999acda68938ef889c9f595a2849da7289e5a6b33e34f5c4d7234e12a730348eb3adff164145acfeba610785c6adc509b7e4cf4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3502,6 +3502,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mapbox/mapbox-gl-rtl-text@npm:0.4.0":
|
||||
version: 0.4.0
|
||||
resolution: "@mapbox/mapbox-gl-rtl-text@npm:0.4.0"
|
||||
checksum: 10/a678240e4cc6f589726ef2f35fac64a7eb073451a8686c09db58041d3fb351d2e72100bb12607591efc298d8ac5e19691c593b3c213d121b2a0e84978254e1a8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mapbox/point-geometry@npm:^1.1.0, @mapbox/point-geometry@npm:~1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@mapbox/point-geometry@npm:1.1.0"
|
||||
@@ -5951,105 +5958,105 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.67.0"
|
||||
"@typescript-eslint/eslint-plugin@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.68.0"
|
||||
dependencies:
|
||||
"@eslint-community/regexpp": "npm:^4.12.2"
|
||||
"@typescript-eslint/scope-manager": "npm:8.67.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.67.0"
|
||||
"@typescript-eslint/utils": "npm:8.67.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.67.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.68.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.68.0"
|
||||
"@typescript-eslint/utils": "npm:8.68.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.68.0"
|
||||
ignore: "npm:^7.0.5"
|
||||
natural-compare: "npm:^1.4.0"
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
"@typescript-eslint/parser": ^8.67.0
|
||||
"@typescript-eslint/parser": ^8.68.0
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/d53f9e51c6be98ff4cdcb94de24e2951f5e2b7a1dfc56c4f8a2d81e61346c511c0478117626d9cfd5d5d63f5e7a0e0653dc1abe1e7d926a3f9806aaf1fa11bd2
|
||||
checksum: 10/8776fd8ffde8eceae3b1aeca57e528e8ee61a683223c3bef455ca3c5e28e52275e6cd6ebf8ee284a46883e6220bbf297312084e1ee85e00cef017a78c169a674
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/parser@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.67.0"
|
||||
"@typescript-eslint/parser@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.68.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager": "npm:8.67.0"
|
||||
"@typescript-eslint/types": "npm:8.67.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.67.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.67.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.68.0"
|
||||
"@typescript-eslint/types": "npm:8.68.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.68.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.68.0"
|
||||
debug: "npm:^4.4.3"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/fa156ade066d0886ea0daa0f872940cdb450a2b5dbd1687e0df64ec19aac86711dde51e17e6b20e72387aa38daec811736821e9db950e8557affa7faea63d65c
|
||||
checksum: 10/979ae0d2c6a391fd5b9b0218189fda5112ff0b5ab2538eef1da4cd77ac8ae46f9b590db9779f2614bd193105b40cf12635b565c840f4cdbbb4a683e24cf5ec4b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/project-service@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.67.0"
|
||||
"@typescript-eslint/project-service@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.68.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.67.0"
|
||||
"@typescript-eslint/types": "npm:^8.67.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.68.0"
|
||||
"@typescript-eslint/types": "npm:^8.68.0"
|
||||
debug: "npm:^4.4.3"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/3beccdee1e74060eac4e6c5e7edd14a800c8c98e3313dbfe5838a357e83babc22c41c3ebe637c331c4b93db2da50f5479ccf4677db7b6b1a760f3cd27795b36f
|
||||
checksum: 10/37521df3731f9069c7df55362342dd0027f43947241de33c2a1c029b4553fe092e2ac69a534153aaf08c862e6b2390d864cb6c88a0714fc276395390df276fb6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/scope-manager@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.67.0"
|
||||
"@typescript-eslint/scope-manager@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.68.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.67.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.67.0"
|
||||
checksum: 10/a0ca8d52e6c5b2538f7df860acd5a36437cce8e6d5bcededdfde04c02489f4b523cb178b502c266d138cff6758ea375ebe311b2b6af373b127ded4876a697ffb
|
||||
"@typescript-eslint/types": "npm:8.68.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.68.0"
|
||||
checksum: 10/494d499315b4f3ab8eaa114aa53a4b2b9f533b82eb007cd5b5617cececfd8ad6938edb74f734479ae6e40a4086f510b6b984de8e66d06c518cb47817549db780
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.67.0, @typescript-eslint/tsconfig-utils@npm:^8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.67.0"
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.68.0, @typescript-eslint/tsconfig-utils@npm:^8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.68.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/736d0ab8a273b4e4e17d412b390386eec0171cd3f7ba451bacd92c4f931051bba2049e040fdae84603ea0e07430afb05b49c6d8b6d429f9012d3c6b03eb69f54
|
||||
checksum: 10/b527a7c5deaad40bb72f78eec1bfea5997a7e792e64666984605e9a167246cf856c8d40290a7b3d39cefb3a242538e22a3a4fc136132e4669a314887e2de5f08
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.67.0"
|
||||
"@typescript-eslint/type-utils@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.68.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.67.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.67.0"
|
||||
"@typescript-eslint/utils": "npm:8.67.0"
|
||||
"@typescript-eslint/types": "npm:8.68.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.68.0"
|
||||
"@typescript-eslint/utils": "npm:8.68.0"
|
||||
debug: "npm:^4.4.3"
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/a0b943e5a8f3c2e8490ec28599eb94561d7ad029fcf42f2e9a0e8d2685ce625898371cd0a7d3a906fe8b4077d72fee8329cad512a5b0521bc2167f7d88a984fb
|
||||
checksum: 10/ed6b6fe74eee017a79c9dbfa72b3a08748dac8de5702314494d8849feee516c20e2907ad730fd82e50be81c5984c575b29e35eb8b5c99091445b4bc5102ce836
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:8.67.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/types@npm:8.67.0"
|
||||
checksum: 10/8edc1a14a52c7566409c19b34fb72ba3a62a3b409c98b1d496de9fcd83179fb128c8df2fde391c199316a54cdc5d314ba70ce7b440161dd850b73a44f3e54e6d
|
||||
"@typescript-eslint/types@npm:8.68.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/types@npm:8.68.0"
|
||||
checksum: 10/f9337e0263d95e6edca6e364f9b1f70d5840b9648b5aa0d02065bcee5809a50c92d024ba17b458182ec4e7fffe5efd05584f1667b1e43ff3e7121b218283a207
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.67.0"
|
||||
"@typescript-eslint/typescript-estree@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.68.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service": "npm:8.67.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.67.0"
|
||||
"@typescript-eslint/types": "npm:8.67.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.67.0"
|
||||
"@typescript-eslint/project-service": "npm:8.68.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.68.0"
|
||||
"@typescript-eslint/types": "npm:8.68.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.68.0"
|
||||
debug: "npm:^4.4.3"
|
||||
minimatch: "npm:^10.2.2"
|
||||
semver: "npm:^7.7.3"
|
||||
@@ -6057,32 +6064,32 @@ __metadata:
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/31c5be812f67c997b9d1e9324c4289b6268a12e9948492403fdd9c9b5df12dcc00f845ae93ae37c78f717583519060175f2147f165b65112eef1e40acf44ad1d
|
||||
checksum: 10/47d75cbc9c10842aeb3c1cc8d0625bd578b3b43b75d3dc86d3543a739b2fc8d0b524fd86b5af1f1493a3f0ae1b9e65c3200bf01a6bde0162efe6414708eac553
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.67.0"
|
||||
"@typescript-eslint/utils@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.68.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.9.1"
|
||||
"@typescript-eslint/scope-manager": "npm:8.67.0"
|
||||
"@typescript-eslint/types": "npm:8.67.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.67.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.68.0"
|
||||
"@typescript-eslint/types": "npm:8.68.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.68.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/8703468cdea3630275c6faacc031e7e4bd7fb8f8988e80ab987811ae3c63a9a9677496cee2b4811f49cb9bc9e6f6c0bdb91cb213353019ce31df0a2b351cea51
|
||||
checksum: 10/6581a1e7ced23cce82279df001d643844c97408bc84826da77ec77d52b77047004b202ff8cfcfbc0b0fef15bf2cafb5ae21bafbda24c9b58102ce634f66c2af9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.67.0"
|
||||
"@typescript-eslint/visitor-keys@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.68.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.67.0"
|
||||
"@typescript-eslint/types": "npm:8.68.0"
|
||||
eslint-visitor-keys: "npm:^5.0.0"
|
||||
checksum: 10/c92c9b202105a8f398ab2ad4be58c8c8fd9af6b5ddd28cdcd57d75f7f04f69561a56a05aac9a008668c64faa7222ebb3fefe19097923a18eb2b9f7a90809a5be
|
||||
checksum: 10/d802bc2c4569495f3d83f9bcf169e3a432ac1c6672e243399886409dbc55d7fa30375d9d0b2cf0d9466b81fc2b01b991483eb56e8cdb8141721009978d3b078b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7750,13 +7757,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cli-spinners@npm:^2.5.0, cli-spinners@npm:^2.6.0":
|
||||
"cli-spinners@npm:^2.5.0":
|
||||
version: 2.9.2
|
||||
resolution: "cli-spinners@npm:2.9.2"
|
||||
checksum: 10/a0a863f442df35ed7294424f5491fa1756bd8d2e4ff0c8736531d886cec0ece4d85e8663b77a5afaf1d296e3cbbebff92e2e99f52bbea89b667cbe789b994794
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cli-spinners@npm:^3.4.0":
|
||||
version: 3.4.0
|
||||
resolution: "cli-spinners@npm:3.4.0"
|
||||
checksum: 10/6a4021c1999011fc34ae714f055dcdafb56309abc1f8fb021ea7d9370dfc524485fe8684226015e5fe6053dd30544e74270184ff7edc3fa4d37043b8efd0a054
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"clipboardy@npm:3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "clipboardy@npm:3.0.0"
|
||||
@@ -8688,7 +8702,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"enquirer@npm:^2.3.6":
|
||||
"enquirer@npm:^2.4.1":
|
||||
version: 2.4.1
|
||||
resolution: "enquirer@npm:2.4.1"
|
||||
dependencies:
|
||||
@@ -9097,9 +9111,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint@npm:10.9.0":
|
||||
version: 10.9.0
|
||||
resolution: "eslint@npm:10.9.0"
|
||||
"eslint@npm:10.9.1":
|
||||
version: 10.9.1
|
||||
resolution: "eslint@npm:10.9.1"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.8.0"
|
||||
"@eslint-community/regexpp": "npm:^4.12.2"
|
||||
@@ -9138,7 +9152,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
eslint: bin/eslint.js
|
||||
checksum: 10/1cd63bbf4f2c003ed759ab6ae9e2030be0abafdcd7b9ffb5be9e9c3aa3ea6142e6af0a337aac21044957750807fde06c9d052e2c9734cca6792dc2d095f9c180
|
||||
checksum: 10/4cee237c7d2c445a05330e595b18521d7452fd3f419241bf1ccf81c769a6761c10ca5a06988197482d4842ed1a26b1dcb51b9526408990cee84559e6c08cb3ab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9675,24 +9689,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"generate-license-file@npm:4.2.1":
|
||||
version: 4.2.1
|
||||
resolution: "generate-license-file@npm:4.2.1"
|
||||
"generate-license-file@npm:4.2.3":
|
||||
version: 4.2.3
|
||||
resolution: "generate-license-file@npm:4.2.3"
|
||||
dependencies:
|
||||
"@commander-js/extra-typings": "npm:^14.0.0"
|
||||
"@npmcli/arborist": "npm:^9.0.0"
|
||||
cli-spinners: "npm:^2.6.0"
|
||||
cli-spinners: "npm:^3.4.0"
|
||||
commander: "npm:^14.0.2"
|
||||
cosmiconfig: "npm:^9.0.0"
|
||||
enquirer: "npm:^2.3.6"
|
||||
enquirer: "npm:^2.4.1"
|
||||
glob: "npm:^13.0.0"
|
||||
json5: "npm:^2.2.3"
|
||||
ora: "npm:^5.4.1"
|
||||
tslib: "npm:^2.3.0"
|
||||
zod: "npm:^4.0.0"
|
||||
bin:
|
||||
generate-license-file: bin/generate-license-file
|
||||
checksum: 10/03876abeba28efea39a0fbd4d048e2c0fe1f59477c14e3a5e9f7022235f9197a3f451b7f26124dc09da5cc63140376f53b4ee9167f238eee58241ab954e62efd
|
||||
checksum: 10/bdfd899c04b247f75115d9710b53124b6f202d85d4cacf3e9d2d46260cc7d75e9f93ebf2204105ecb49b051c5acfac6c05d1a9711763b7daed073bffd986ac69
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -10147,7 +10160,8 @@ __metadata:
|
||||
"@lit/context": "npm:1.1.6"
|
||||
"@lit/reactive-element": "npm:2.1.2"
|
||||
"@lit/task": "npm:1.0.3"
|
||||
"@lokalise/node-api": "npm:16.3.0"
|
||||
"@lokalise/node-api": "npm:16.4.0"
|
||||
"@mapbox/mapbox-gl-rtl-text": "npm:0.4.0"
|
||||
"@maplibre/maplibre-gl-leaflet": "npm:0.1.4"
|
||||
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch"
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
||||
@@ -10206,7 +10220,7 @@ __metadata:
|
||||
echarts: "npm:6.1.0"
|
||||
echarts-extension-chart2music: "npm:0.1.1"
|
||||
element-internals-polyfill: "npm:3.0.2"
|
||||
eslint: "npm:10.9.0"
|
||||
eslint: "npm:10.9.1"
|
||||
eslint-config-prettier: "npm:10.1.8"
|
||||
eslint-import-resolver-webpack: "npm:0.13.11"
|
||||
eslint-plugin-import-x: "npm:4.17.1"
|
||||
@@ -10217,7 +10231,7 @@ __metadata:
|
||||
fancy-log: "npm:2.0.0"
|
||||
fs-extra: "npm:11.4.0"
|
||||
fuse.js: "npm:7.5.0"
|
||||
generate-license-file: "npm:4.2.1"
|
||||
generate-license-file: "npm:4.2.3"
|
||||
glob: "npm:13.0.6"
|
||||
globals: "npm:17.11.0"
|
||||
gulp: "npm:5.0.1"
|
||||
@@ -10229,7 +10243,7 @@ __metadata:
|
||||
husky: "npm:9.1.7"
|
||||
idb-keyval: "npm:6.3.0"
|
||||
intl-messageformat: "npm:11.2.14"
|
||||
js-yaml: "npm:5.3.0"
|
||||
js-yaml: "npm:5.4.0"
|
||||
jsdom: "npm:30.0.1"
|
||||
jszip: "npm:3.10.1"
|
||||
leaflet: "npm:1.9.4"
|
||||
@@ -10246,7 +10260,7 @@ __metadata:
|
||||
luxon: "npm:3.7.2"
|
||||
map-stream: "npm:0.0.7"
|
||||
maplibre-gl: "npm:5.24.0"
|
||||
marked: "npm:18.0.10"
|
||||
marked: "npm:18.0.11"
|
||||
memoize-one: "npm:6.0.0"
|
||||
minify-literals: "npm:2.2.0"
|
||||
node-vibrant: "npm:4.0.4"
|
||||
@@ -10269,7 +10283,7 @@ __metadata:
|
||||
tinykeys: "patch:tinykeys@npm%3A4.0.0#~/.yarn/patches/tinykeys-npm-4.0.0-a6ca3fd771.patch"
|
||||
ts-lit-plugin: "npm:2.0.2"
|
||||
typescript: "npm:6.0.3"
|
||||
typescript-eslint: "npm:8.67.0"
|
||||
typescript-eslint: "npm:8.68.0"
|
||||
vite-tsconfig-paths: "npm:6.1.1"
|
||||
vitest: "npm:4.1.11"
|
||||
webpack-stats-plugin: "npm:1.1.3"
|
||||
@@ -11177,14 +11191,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-yaml@npm:5.3.0":
|
||||
version: 5.3.0
|
||||
resolution: "js-yaml@npm:5.3.0"
|
||||
"js-yaml@npm:5.4.0":
|
||||
version: 5.4.0
|
||||
resolution: "js-yaml@npm:5.4.0"
|
||||
dependencies:
|
||||
argparse: "npm:^2.0.1"
|
||||
bin:
|
||||
js-yaml: bin/js-yaml.mjs
|
||||
checksum: 10/6c7e8ea8dd5643d9df73f4aa796f3572537284e9b819b74ef919467452bfa4624f8f7477cfc775c91db7e2f32387133b1b6c4e95aef9702be6bb042ae554884a
|
||||
checksum: 10/d8216083212772b203a1ee16be2ea96a1638c6d7f0ff769ba4da274b5ad29e858bd26d96761feb8937fce1aa47e8acfa79b2cd21a8ced724cc5555c11b4d056c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -11944,12 +11958,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"marked@npm:18.0.10":
|
||||
version: 18.0.10
|
||||
resolution: "marked@npm:18.0.10"
|
||||
"marked@npm:18.0.11":
|
||||
version: 18.0.11
|
||||
resolution: "marked@npm:18.0.11"
|
||||
bin:
|
||||
marked: bin/marked.js
|
||||
checksum: 10/0d4b560e0773fd6ba30a4e7560ba7ae1f05d47b23926ad8337d9f80c598dfdbffdf2f81e773a1789e8bc9d9c2c2d3d0c9143a54d32581c336a3bbb1bad80461c
|
||||
checksum: 10/6da69f17820c7442358d751e902d3e0e06fef957e1178d634db938cf339c4504399c9f11a953b0a116b1e5bebc1c7fb52726777cc6c351f2c9f39e4a8276e21a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -15232,18 +15246,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript-eslint@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "typescript-eslint@npm:8.67.0"
|
||||
"typescript-eslint@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "typescript-eslint@npm:8.68.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.67.0"
|
||||
"@typescript-eslint/parser": "npm:8.67.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.67.0"
|
||||
"@typescript-eslint/utils": "npm:8.67.0"
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.68.0"
|
||||
"@typescript-eslint/parser": "npm:8.68.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.68.0"
|
||||
"@typescript-eslint/utils": "npm:8.68.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/4efc2cea5b067f65d58e010fda3888ec94f2e67ebfa289be78c3a02458787b7125cf8f6095776b912005fb69e8748ad4018777b16436a0658957c163ef213bf9
|
||||
checksum: 10/451c64296ac5f48e4621a03324f5aaef4a26f1dd9d4df63bd3cb07f136e8379185a800731f83634eb5d909c696fad820ba74d31ab3d7cf263328504275d1305b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user